Commit f44fb5cf authored by Clement Ho's avatar Clement Ho

Add filtered search visual tokens

parent b5cb1115
...@@ -37,11 +37,14 @@ require('../window')(function(w){ ...@@ -37,11 +37,14 @@ require('../window')(function(w){
} }
} }
if (!self.destroyed) {
self.hook.list[config.method].call(self.hook.list, data); self.hook.list[config.method].call(self.hook.list, data);
}
}, },
init: function init(hook) { init: function init(hook) {
var self = this; var self = this;
self.destroyed = false;
self.cache = self.cache || {}; self.cache = self.cache || {};
var config = hook.config.droplabAjax; var config = hook.config.droplabAjax;
this.hook = hook; this.hook = hook;
...@@ -79,6 +82,7 @@ require('../window')(function(w){ ...@@ -79,6 +82,7 @@ require('../window')(function(w){
destroy: function() { destroy: function() {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
this.destroyed = true;
if (this.listTemplate && dynamicList) { if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate; dynamicList.outerHTML = this.listTemplate;
} }
......
...@@ -28,6 +28,23 @@ require('./filtered_search_dropdown'); ...@@ -28,6 +28,23 @@ require('./filtered_search_dropdown');
const tag = selected.querySelector('.js-filter-tag').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) { if (tag.length) {
// Get previous input values in the input field and convert them into visual tokens
const previousInputValues = this.input.value.split(' ');
const searchTerms = [];
previousInputValues.forEach((value, index) => {
searchTerms.push(value);
if (index === previousInputValues.length - 1
&& token.indexOf(value.toLowerCase()) !== -1) {
searchTerms.pop();
}
});
if (searchTerms.length > 0) {
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
}
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
} }
this.dismissDropdown(); this.dismissDropdown();
...@@ -39,7 +56,7 @@ require('./filtered_search_dropdown'); ...@@ -39,7 +56,7 @@ require('./filtered_search_dropdown');
renderContent() { renderContent() {
const dropdownData = []; const dropdownData = [];
[].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => { [].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
const { icon, hint, tag } = dropdownMenu.dataset; const { icon, hint, tag } = dropdownMenu.dataset;
if (icon && hint && tag) { if (icon && hint && tag) {
dropdownData.push({ dropdownData.push({
......
...@@ -39,7 +39,12 @@ require('./filtered_search_dropdown'); ...@@ -39,7 +39,12 @@ require('./filtered_search_dropdown');
getSearchInput() { getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input); const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
let value = lastToken.value || '';
let value = lastToken || '';
if (value[0] === '@') {
value = value.slice(1);
}
// Removes the first character if it is a quotation so that we can search // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
......
...@@ -22,12 +22,17 @@ ...@@ -22,12 +22,17 @@
static filterWithSymbol(filterSymbol, input, item) { static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item; const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input); const searchInput = gl.DropdownUtils.getSearchInput(input);
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
if (lastToken !== searchToken) {
const title = updatedItem.title.toLowerCase(); const title = updatedItem.title.toLowerCase();
let value = lastToken.value.toLowerCase(); let value = searchInput.toLowerCase();
let symbol = '';
// Remove the symbol for filter
if (value[0] === filterSymbol) {
symbol = value[0];
value = value.slice(1);
}
// Removes the first character if it is a quotation so that we can search // Removes the first character if it is a quotation so that we can search
// with multiple words // with multiple words
...@@ -36,24 +41,21 @@ ...@@ -36,24 +41,21 @@
} }
// Eg. filterSymbol = ~ for labels // Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; const match = title.indexOf(`${symbol}${value}`) !== -1;
updatedItem.droplab_hidden = !match && !matchWithoutSymbol; updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
} else {
updatedItem.droplab_hidden = false;
}
return updatedItem; return updatedItem;
} }
static filterHint(input, item) { static filterHint(input, item) {
const updatedItem = item; const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input); const searchInput = gl.DropdownUtils.getSearchInput(input);
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); let { lastToken } = gl.FilteredSearchTokenizer.processTokens(searchInput);
lastToken = lastToken.key || lastToken || ''; lastToken = lastToken.key || lastToken || '';
if (!lastToken || query.split('').last() === ' ') { if (!lastToken || searchInput.split('').last() === ' ') {
updatedItem.droplab_hidden = false; updatedItem.droplab_hidden = false;
} else if (lastToken) { } else if (lastToken) {
const split = lastToken.split(':'); const split = lastToken.split(':');
...@@ -70,13 +72,40 @@ ...@@ -70,13 +72,40 @@
const dataValue = selected.getAttribute('data-value'); const dataValue = selected.getAttribute('data-value');
if (dataValue) { if (dataValue) {
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
} }
// Return boolean based on whether it was set // Return boolean based on whether it was set
return dataValue !== null; return dataValue !== null;
} }
static getSearchQuery() {
const tokensContainer = document.querySelector('.tokens-container');
const values = [];
[].forEach.call(tokensContainer.querySelectorAll('.js-visual-token'), (token) => {
const name = token.querySelector('.name');
const value = token.querySelector('.value');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
if (value && value.innerText) {
valueText = value.innerText;
}
if (token.className.indexOf('filtered-search-token') !== -1) {
values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
} else {
values.push(name.innerText);
}
});
const input = document.querySelector('.filtered-search');
values.push(input && input.value);
return values.join(' ');
}
static getSearchInput(filteredSearchInput) { static getSearchInput(filteredSearchInput) {
const inputValue = filteredSearchInput.value; const inputValue = filteredSearchInput.value;
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
......
...@@ -7,3 +7,4 @@ require('./filtered_search_dropdown'); ...@@ -7,3 +7,4 @@ require('./filtered_search_dropdown');
require('./filtered_search_manager'); require('./filtered_search_manager');
require('./filtered_search_token_keys'); require('./filtered_search_token_keys');
require('./filtered_search_tokenizer'); require('./filtered_search_tokenizer');
require('./filtered_search_visual_tokens');
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
if (!dataValueSet) { if (!dataValueSet) {
const value = getValueFunction(selected); const value = getValueFunction(selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
} }
this.dismissDropdown(); this.dismissDropdown();
......
...@@ -58,35 +58,15 @@ ...@@ -58,35 +58,15 @@
}; };
} }
static addWordToInput(tokenName, tokenValue = '') { static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = document.querySelector('.filtered-search'); const input = document.querySelector('.filtered-search');
const inputValue = input.value;
const word = `${tokenName}:${tokenValue}`;
// Get the string to replace gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
let newCaretPosition = input.selectionStart; input.value = '';
const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; if (clicked) {
gl.FilteredSearchVisualTokens.moveInputToTheRight();
// If we have added a tokenValue at the end of the input,
// add a space and set selection to the end
if (right >= inputValue.length && tokenValue !== '') {
input.value += ' ';
newCaretPosition = input.value.length;
}
gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
} }
static updateInputCaretPosition(selectionStart, input) {
// Reset the position
// Sometimes can end up at end of input
input.setSelectionRange(selectionStart, selectionStart);
const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
input.setSelectionRange(right, right);
} }
updateCurrentDropdownOffset() { updateCurrentDropdownOffset() {
...@@ -94,19 +74,14 @@ ...@@ -94,19 +74,14 @@
} }
updateDropdownOffset(key) { updateDropdownOffset(key) {
if (!this.font) { // Always align dropdown with the input field
this.font = window.getComputedStyle(this.filteredSearchInput).font; let offset = this.filteredSearchInput.getBoundingClientRect().left - document.querySelector('.scroll-container').getBoundingClientRect().left;
}
const input = this.filteredSearchInput;
const inputText = input.value.slice(0, input.selectionStart);
const filterIconPadding = 27;
let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : const maxInputWidth = 240;
this.mapping[key].element.clientWidth; const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
// Make sure offset never exceeds the input container
const offsetMaxWidth = document.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) { if (offsetMaxWidth < offset) {
offset = offsetMaxWidth; offset = offsetMaxWidth;
} }
...@@ -164,8 +139,8 @@ ...@@ -164,8 +139,8 @@
} }
setDropdown() { setDropdown() {
const { lastToken, searchToken } = this.tokenizer const query = gl.DropdownUtils.getSearchQuery();
.processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); const { lastToken, searchToken } = this.tokenizer.processTokens(query);
if (this.currentDropdown) { if (this.currentDropdown) {
this.updateCurrentDropdownOffset(); this.updateCurrentDropdownOffset();
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
constructor(page) { constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search'); this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search'); this.clearSearchButton = document.querySelector('.clear-search');
this.tokensContainer = document.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (this.filteredSearchInput) { if (this.filteredSearchInput) {
...@@ -27,36 +28,61 @@ ...@@ -27,36 +28,61 @@
this.handleFormSubmit = this.handleFormSubmit.bind(this); this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); 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.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.tokenChange = this.tokenChange.bind(this); this.tokenChange = this.tokenChange.bind(this);
this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); 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('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange); this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', FilteredSearchManager.editToken);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
} }
unbindEvents() { unbindEvents() {
this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); 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('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange); this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', FilteredSearchManager.editToken);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
} }
checkForBackspace(e) { checkForBackspace(e) {
// 8 = Backspace Key // 8 = Backspace Key
// 46 = Delete Key // 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (this.filteredSearchInput.value === '' && lastVisualToken) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
}
// Reposition dropdown so that it is aligned with cursor // Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset(); this.dropdownManager.updateCurrentDropdownOffset();
} }
...@@ -86,11 +112,67 @@ ...@@ -86,11 +112,67 @@
} }
} }
toggleClearSearchButton(e) { static selectToken(e) {
if (e.target.value) { const button = e.target.closest('.selectable');
this.clearSearchButton.classList.remove('hidden');
} else { if (button) {
this.clearSearchButton.classList.add('hidden'); e.preventDefault();
e.stopPropagation();
gl.FilteredSearchVisualTokens.selectToken(button);
}
}
unselectEditTokens(e) {
const inputContainer = document.querySelector('.filtered-search-input-container');
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) {
gl.FilteredSearchVisualTokens.moveInputToTheRight();
this.dropdownManager.resetDropdowns();
}
}
static editToken(e) {
const token = e.target.closest('.js-visual-token');
if (token) {
gl.FilteredSearchVisualTokens.editToken(token);
}
}
toggleClearSearchButton() {
const query = gl.DropdownUtils.getSearchQuery();
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() {
const query = gl.DropdownUtils.getSearchQuery();
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 = '';
}
}
removeSelectedToken(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
} }
} }
...@@ -98,11 +180,67 @@ ...@@ -98,11 +180,67 @@
e.preventDefault(); e.preventDefault();
this.filteredSearchInput.value = ''; this.filteredSearchInput.value = '';
const removeElements = [];
[].forEach.call(this.tokensContainer.children, (t) => {
if (t.classList.contains('js-visual-token')) {
removeElements.push(t);
}
});
removeElements.forEach((el) => {
el.parentElement.removeChild(el);
});
this.clearSearchButton.classList.add('hidden'); this.clearSearchButton.classList.add('hidden');
this.handleInputPlaceholder();
this.dropdownManager.resetDropdowns(); this.dropdownManager.resetDropdowns();
} }
handleInputVisualToken() {
const input = this.filteredSearchInput;
const { tokens, searchToken }
= gl.FilteredSearchTokenizer.processTokens(input.value);
const { isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) {
tokens.forEach((t) => {
input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
});
const fragments = searchToken.split(':');
if (fragments.length > 1) {
const inputValues = fragments[0].split(' ');
const tokenKey = inputValues.last();
if (inputValues.length > 1) {
inputValues.pop();
const searchTerms = inputValues.join(' ');
input.value = input.value.replace(searchTerms, '');
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
}
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
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] === ' ') {
gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
// Trim the last space as seen in the if statement above
input.value = input.value.replace(searchToken, '').trim();
}
}
}
handleFormSubmit(e) { handleFormSubmit(e) {
e.preventDefault(); e.preventDefault();
this.search(); this.search();
...@@ -111,7 +249,7 @@ ...@@ -111,7 +249,7 @@
loadSearchParamsFromURL() { loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray(); const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams(); const usernameParams = this.getUsernameParams();
const inputValues = []; let hasFilteredSearch = false;
params.forEach((p) => { params.forEach((p) => {
const split = p.split('='); const split = p.split('=');
...@@ -122,7 +260,8 @@ ...@@ -122,7 +260,8 @@
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p); const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) { if (condition) {
inputValues.push(`${condition.tokenKey}:${condition.value}`); hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
} else { } else {
// Sanitize value since URL converts spaces into + // Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded + // Replace before decode so that we know what was originally + versus the encoded +
...@@ -140,34 +279,37 @@ ...@@ -140,34 +279,37 @@
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
} }
inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
} else if (!match && keyParam === 'assignee_id') { } else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10); const id = parseInt(value, 10);
if (usernameParams[id]) { if (usernameParams[id]) {
inputValues.push(`assignee:@${usernameParams[id]}`); hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
} }
} else if (!match && keyParam === 'author_id') { } else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10); const id = parseInt(value, 10);
if (usernameParams[id]) { if (usernameParams[id]) {
inputValues.push(`author:@${usernameParams[id]}`); hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
} }
} else if (!match && keyParam === 'search') { } else if (!match && keyParam === 'search') {
inputValues.push(sanitizedValue); hasFilteredSearch = true;
this.filteredSearchInput.value = sanitizedValue;
} }
} }
}); });
// Trim the last space value if (hasFilteredSearch) {
this.filteredSearchInput.value = inputValues.join(' ');
if (inputValues.length > 0) {
this.clearSearchButton.classList.remove('hidden'); this.clearSearchButton.classList.remove('hidden');
this.handleInputPlaceholder();
} }
} }
search() { search() {
const paths = []; const paths = [];
const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); const { tokens, searchToken }
= this.tokenizer.processTokens(gl.DropdownUtils.getSearchQuery());
const currentState = gl.utils.getParameterByName('state') || 'opened'; const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`); paths.push(`state=${currentState}`);
......
class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
const inputLi = document.querySelector('.input-token');
const lastVisualToken = inputLi && inputLi.previousElementSibling;
return {
lastVisualToken,
isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null),
};
}
static unselectTokens() {
const otherTokens = document.querySelectorAll('.js-visual-token .selectable.selected');
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
}
static selectToken(tokenButton) {
const selected = tokenButton.classList.contains('selected');
FilteredSearchVisualTokens.unselectTokens();
if (!selected) {
tokenButton.classList.add('selected');
}
}
static removeSelectedToken() {
const selected = document.querySelector('.js-visual-token .selected');
if (selected) {
const li = selected.closest('.js-visual-token');
li.parentElement.removeChild(li);
}
}
static createVisualTokenElementHTML() {
return `
<div class="selectable" role="button">
<div class="name"></div>
<div class="value"></div>
</div>
`;
}
static addVisualTokenElement(name, value, isSearchTerm) {
const li = document.createElement('li');
li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
if (value) {
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
li.querySelector('.value').innerText = value;
} else {
li.innerHTML = '<div class="name"></div>';
}
li.querySelector('.name').innerText = name;
const tokensContainer = document.querySelector('.tokens-container');
const input = document.querySelector('.filtered-search');
tokensContainer.insertBefore(li, input.parentElement);
}
static addValueToPreviousVisualTokenElement(value) {
const { lastVisualToken, isLastVisualTokenValid } =
FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
const name = FilteredSearchVisualTokens.getLastTokenPartial();
lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
lastVisualToken.querySelector('.name').innerText = name;
lastVisualToken.querySelector('.value').innerText = value;
}
}
static addFilterVisualToken(tokenName, tokenValue) {
const { lastVisualToken, isLastVisualTokenValid }
= FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
if (isLastVisualTokenValid) {
addVisualTokenElement(tokenName, tokenValue);
} else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
const tokensContainer = document.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken);
const value = tokenValue || tokenName;
addVisualTokenElement(previousTokenName, value);
}
}
static addSearchVisualToken(searchTerm) {
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
} else {
FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
}
}
static getLastTokenPartial() {
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (!lastVisualToken) return '';
const value = lastVisualToken.querySelector('.value');
const name = lastVisualToken.querySelector('.name');
const valueText = value ? value.innerText : '';
const nameText = name ? name.innerText : '';
return valueText || nameText;
}
static removeLastTokenPartial() {
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (lastVisualToken) {
const value = lastVisualToken.querySelector('.value');
if (value) {
const button = lastVisualToken.querySelector('.selectable');
button.removeChild(value);
lastVisualToken.innerHTML = button.innerHTML;
} else {
lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
}
}
}
static tokenizeInput() {
const input = document.querySelector('.filtered-search');
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (input.value) {
if (isLastVisualTokenValid) {
gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value);
} else {
FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value);
}
input.value = '';
}
}
static editToken(token) {
const input = document.querySelector('.filtered-search');
FilteredSearchVisualTokens.tokenizeInput();
// Replace token with input field
const tokenContainer = token.parentElement;
const inputLi = input.parentElement;
tokenContainer.replaceChild(inputLi, token);
const name = token.querySelector('.name');
const value = token.querySelector('.value');
if (token.classList.contains('filtered-search-token')) {
FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
input.value = value.innerText;
} else {
// token is a search term
input.value = name.innerText;
}
// Opens dropdown
const inputEvent = new Event('input');
input.dispatchEvent(inputEvent);
// Adds cursor to input
input.focus();
}
static moveInputToTheRight() {
const input = document.querySelector('.filtered-search');
const inputLi = input.parentElement;
const tokenContainer = document.querySelector('.tokens-container');
if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
FilteredSearchVisualTokens.tokenizeInput();
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (!isLastVisualTokenValid) {
const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
}
tokenContainer.removeChild(inputLi);
tokenContainer.appendChild(inputLi);
}
}
}
window.gl = window.gl || {};
gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens;
...@@ -64,6 +64,89 @@ ...@@ -64,6 +64,89 @@
-webkit-flex-direction: column; -webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
} }
.tokens-container {
display: -webkit-flex;
display: flex;
flex: 1;
-webkit-flex: 1;
padding-left: 30px;
position: relative;
margin-bottom: 0;
}
.input-token {
flex: 1;
-webkit-flex: 1;
}
.filtered-search-token + .input-token:not(:last-child) {
max-width: 200px;
}
}
.filtered-search-token,
.filtered-search-term {
display: -webkit-flex;
display: flex;
margin-top: 5px;
margin-bottom: 5px;
.selectable {
display: -webkit-flex;
display: flex;
}
.name,
.value {
display: inline-block;
padding: 2px 7px;
}
.name {
background-color: $filter-name-resting-color;
color: $filter-name-text-color;
border-radius: 2px 0 0 2px;
margin-right: 1px;
text-transform: capitalize;
}
.value {
background-color: $white-normal;
color: $filter-value-text-color;
border-radius: 0 2px 2px 0;
margin-right: 5px;
}
.selected {
.name {
background-color: $filter-name-selected-color;
}
.value {
background-color: $filter-value-selected-color;
}
}
}
.filtered-search-term {
.name {
background-color: inherit;
color: $black;
text-transform: none;
}
.selectable {
cursor: text;
}
}
.scroll-container {
display: -webkit-flex;
display: flex;
overflow-x: scroll;
white-space: nowrap;
width: 100%;
} }
.filtered-search-input-container { .filtered-search-input-container {
...@@ -71,6 +154,9 @@ ...@@ -71,6 +154,9 @@
display: flex; display: flex;
position: relative; position: relative;
width: 100%; width: 100%;
border: 1px solid $border-color;
background-color: $white-light;
max-width: 87%;
@media (max-width: $screen-xs-min) { @media (max-width: $screen-xs-min) {
-webkit-flex: 1 1 100%; -webkit-flex: 1 1 100%;
...@@ -87,12 +173,22 @@ ...@@ -87,12 +173,22 @@
} }
.form-control { .form-control {
padding-left: 25px; position: relative;
min-width: 200px;
padding-left: 0;
padding-right: 25px; padding-right: 25px;
border-color: transparent;
&:focus ~ .fa-filter { &:focus ~ .fa-filter {
color: $common-gray-dark; color: $common-gray-dark;
} }
&:focus,
&:hover {
outline: none;
border-color: transparent;
box-shadow: none;
}
} }
.fa-filter { .fa-filter {
...@@ -109,12 +205,13 @@ ...@@ -109,12 +205,13 @@
.clear-search { .clear-search {
width: 35px; width: 35px;
background-color: transparent; background-color: $white-light;
border: none; border: none;
position: absolute; position: absolute;
right: 0; right: 0;
height: 100%; height: 100%;
outline: none; outline: none;
z-index: 1;
&:hover .fa-times { &:hover .fa-times {
color: $common-gray-dark; color: $common-gray-dark;
......
...@@ -540,3 +540,12 @@ Pipeline Graph ...@@ -540,3 +540,12 @@ Pipeline Graph
$stage-hover-bg: #eaf3fc; $stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc; $stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6; $action-icon-color: #d6d6d6;
/*
Filtered Search
*/
$filter-name-resting-color: #f8f8f8;
$filter-name-text-color: rgba(0, 0, 0, 0.55);
$filter-value-text-color: rgba(0, 0, 0, 0.85);
$filter-name-selected-color: #ebebeb;
$filter-value-selected-color: #d7d7d7;
...@@ -11,6 +11,9 @@ ...@@ -11,6 +11,9 @@
class: "check_all_issues left" class: "check_all_issues left"
.issues-other-filters.filtered-search-container .issues-other-filters.filtered-search-container
.filtered-search-input-container .filtered-search-input-container
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) } %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
= icon('filter') = icon('filter')
%button.clear-search.hidden{ type: 'button' } %button.clear-search.hidden{ type: 'button' }
......
require 'rails_helper' require 'rails_helper'
describe 'Dropdown assignee', :feature, :js do describe 'Dropdown assignee', :feature, :js do
include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') } let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
...@@ -133,7 +136,8 @@ describe 'Dropdown assignee', :feature, :js do ...@@ -133,7 +136,8 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee(user_jacob.name) click_assignee(user_jacob.name)
expect(page).to have_css(js_dropdown_assignee, visible: false) expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ") expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }])
expect_filtered_search_input_empty
end end
it 'fills in the assignee username when the assignee has been filtered' do it 'fills in the assignee username when the assignee has been filtered' do
...@@ -141,14 +145,16 @@ describe 'Dropdown assignee', :feature, :js do ...@@ -141,14 +145,16 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee(user.name) click_assignee(user.name)
expect(page).to have_css(js_dropdown_assignee, visible: false) expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:@#{user.username} ") expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end end
it 'selects `no assignee`' do it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false) expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:none ") expect_tokens([{ name: 'assignee', value: 'none' }])
expect_filtered_search_input_empty
end end
end end
......
require 'rails_helper' require 'rails_helper'
describe 'Dropdown author', js: true, feature: true do describe 'Dropdown author', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
...@@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do ...@@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do
click_author(user_jacob.name) click_author(user_jacob.name)
expect(page).to have_css(js_dropdown_author, visible: false) expect(page).to have_css(js_dropdown_author, visible: false)
expect(filtered_search.value).to eq("author:@#{user_jacob.username} ") expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }])
expect_filtered_search_input_empty
end end
it 'fills in the author username when the author has been filtered' do it 'fills in the author username when the author has been filtered' do
click_author(user.name) click_author(user.name)
expect(page).to have_css(js_dropdown_author, visible: false) expect(page).to have_css(js_dropdown_author, visible: false)
expect(filtered_search.value).to eq("author:@#{user.username} ") expect_tokens([{ name: 'author', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end end
end end
......
require 'rails_helper' require 'rails_helper'
describe 'Dropdown hint', js: true, feature: true do describe 'Dropdown hint', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
...@@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true) expect(page).to have_css('#js-dropdown-author', visible: true)
expect(filtered_search.value).to eq('author:') expect_tokens([{ name: 'author' }])
expect_filtered_search_input_empty
end end
it 'opens the assignee dropdown when you click on assignee' do it 'opens the assignee dropdown when you click on assignee' do
...@@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(page).to have_css('#js-dropdown-assignee', visible: true)
expect(filtered_search.value).to eq('assignee:') expect_tokens([{ name: 'assignee' }])
expect_filtered_search_input_empty
end end
it 'opens the milestone dropdown when you click on milestone' do it 'opens the milestone dropdown when you click on milestone' do
...@@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(page).to have_css('#js-dropdown-milestone', visible: true)
expect(filtered_search.value).to eq('milestone:') expect_tokens([{ name: 'milestone' }])
expect_filtered_search_input_empty
end end
it 'opens the label dropdown when you click on label' do it 'opens the label dropdown when you click on label' do
...@@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true) expect(page).to have_css('#js-dropdown-label', visible: true)
expect(filtered_search.value).to eq('label:') expect_tokens([{ name: 'label' }])
expect_filtered_search_input_empty
end end
end end
...@@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true) expect(page).to have_css('#js-dropdown-author', visible: true)
expect(filtered_search.value).to eq('author:') expect_tokens([{ name: 'author' }])
expect_filtered_search_input_empty
end end
it 'opens the assignee dropdown when you click on assignee' do it 'opens the assignee dropdown when you click on assignee' do
...@@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(page).to have_css('#js-dropdown-assignee', visible: true)
expect(filtered_search.value).to eq('assignee:') expect_tokens([{ name: 'assignee' }])
expect_filtered_search_input_empty
end end
it 'opens the milestone dropdown when you click on milestone' do it 'opens the milestone dropdown when you click on milestone' do
...@@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(page).to have_css('#js-dropdown-milestone', visible: true)
expect(filtered_search.value).to eq('milestone:') expect_tokens([{ name: 'milestone' }])
expect_filtered_search_input_empty
end end
it 'opens the label dropdown when you click on label' do it 'opens the label dropdown when you click on label' do
...@@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do ...@@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true) expect(page).to have_css('#js-dropdown-label', visible: true)
expect(filtered_search.value).to eq('label:') expect_tokens([{ name: 'label' }])
expect_filtered_search_input_empty
end
end
describe 'reselecting from dropdown' do
it 'reuses existing author text' do
filtered_search.send_keys('author:')
filtered_search.send_keys(:backspace)
click_hint('author')
expect_tokens([{ name: 'author' }])
expect_filtered_search_input_empty
end
it 'reuses existing assignee text' do
filtered_search.send_keys('assignee:')
filtered_search.send_keys(:backspace)
click_hint('assignee')
expect_tokens([{ name: 'assignee' }])
expect_filtered_search_input_empty
end
it 'reuses existing milestone text' do
filtered_search.send_keys('milestone:')
filtered_search.send_keys(:backspace)
click_hint('milestone')
expect_tokens([{ name: 'milestone' }])
expect_filtered_search_input_empty
end
it 'reuses existing label text' do
filtered_search.send_keys('label:')
filtered_search.send_keys(:backspace)
click_hint('label')
expect_tokens([{ name: 'label' }])
expect_filtered_search_input_empty
end end
end end
end end
...@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.native.send_keys(:down, :down, :enter) filtered_search.native.send_keys(:down, :down, :enter)
expect(filtered_search.value).to eq("label:~#{bug_label.title} ") expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
expect_filtered_search_input_empty
end end
end end
...@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do
end end
it 'filters by case-insensitive name with or without symbol' do it 'filters by case-insensitive name with or without symbol' do
search_for_label('b') filtered_search.send_keys('b')
expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
...@@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do
clear_search_field clear_search_field
init_label_search init_label_search
search_for_label('~bu') filtered_search.send_keys('~bu')
expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
...@@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title) click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label) expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{bug_label.title} ") expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
expect_filtered_search_input_empty
end end
it 'fills in the label name when the label is partially filled' do it 'fills in the label name when the label is partially filled' do
...@@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do ...@@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title) click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label) expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{bug_label.title} ") expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
expect_filtered_search_input_empty
end end
it 'fills in the label name that contains multiple words' do it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title) click_label(two_words_label.title)
expect(page).not_to have_css(js_dropdown_label) expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ") expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }])
expect_filtered_search_input_empty
end end
it 'fills in the label name that contains multiple words and is very long' do it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title) click_label(long_label.title)
expect(page).not_to have_css(js_dropdown_label) expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ") expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }])
expect_filtered_search_input_empty
end end
it 'fills in the label name that contains double quotes' do it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title) click_label(wont_fix_label.title)
expect(page).not_to have_css(js_dropdown_label) expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ") expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }])
expect_filtered_search_input_empty
end end
it 'fills in the label name with the correct capitalization' do it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title) click_label(uppercase_label.title)
expect(page).not_to have_css(js_dropdown_label) expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ") expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }])
expect_filtered_search_input_empty
end end
it 'fills in the label name with special characters' do it 'fills in the label name with special characters' do
click_label(special_label.title) click_label(special_label.title)
expect(page).not_to have_css(js_dropdown_label) expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{special_label.title} ") expect_tokens([{ name: 'label', value: "~#{special_label.title}" }])
expect_filtered_search_input_empty
end end
it 'selects `no label`' do it 'selects `no label`' do
find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
expect(page).not_to have_css(js_dropdown_label) expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:none ") expect_tokens([{ name: 'label', value: 'none' }])
expect_filtered_search_input_empty
end end
end end
......
require 'rails_helper' require 'rails_helper'
describe 'Dropdown milestone', js: true, feature: true do describe 'Dropdown milestone', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
...@@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do ...@@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title) click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
expect_filtered_search_input_empty
end end
it 'fills in the milestone name when the milestone is partially filled' do it 'fills in the milestone name when the milestone is partially filled' do
...@@ -135,56 +137,64 @@ describe 'Dropdown milestone', js: true, feature: true do ...@@ -135,56 +137,64 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title) click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{milestone.title} ") expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
expect_filtered_search_input_empty
end end
it 'fills in the milestone name that contains multiple words' do it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title) click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ") expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }])
expect_filtered_search_input_empty
end end
it 'fills in the milestone name that contains multiple words and is very long' do it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title) click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ") expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }])
expect_filtered_search_input_empty
end end
it 'fills in the milestone name that contains double quotes' do it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title) click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ") expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }])
expect_filtered_search_input_empty
end end
it 'fills in the milestone name with the correct capitalization' do it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title) click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ") expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }])
expect_filtered_search_input_empty
end end
it 'fills in the milestone name with special characters' do it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title) click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ") expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }])
expect_filtered_search_input_empty
end end
it 'selects `no milestone`' do it 'selects `no milestone`' do
click_static_milestone('No Milestone') click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:none ") expect_tokens([{ name: 'milestone', value: 'none' }])
expect_filtered_search_input_empty
end end
it 'selects `upcoming milestone`' do it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming') click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false) expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:upcoming ") expect_tokens([{ name: 'milestone', value: 'upcoming' }])
expect_filtered_search_input_empty
end end
end end
......
require 'rails_helper' require 'spec_helper'
describe 'Filter issues', js: true, feature: true do describe 'Filter issues', js: true, feature: true do
include FilteredSearchHelpers include FilteredSearchHelpers
...@@ -97,7 +97,9 @@ describe 'Filter issues', js: true, feature: true do ...@@ -97,7 +97,9 @@ describe 'Filter issues', js: true, feature: true do
it 'filters issues by searched author' do it 'filters issues by searched author' do
input_filtered_search("author:@#{user.username}") input_filtered_search("author:@#{user.username}")
expect_tokens([{ name: 'author', value: user.username }])
expect_issues_list_count(5) expect_issues_list_count(5)
expect_filtered_search_input_empty
end end
it 'filters issues by invalid author' do it 'filters issues by invalid author' do
...@@ -110,36 +112,50 @@ describe 'Filter issues', js: true, feature: true do ...@@ -110,36 +112,50 @@ describe 'Filter issues', js: true, feature: true do
end end
context 'author with other filters' do context 'author with other filters' do
search_term = 'issue'
it 'filters issues by searched author and text' do it 'filters issues by searched author and text' do
search = "author:@#{user.username} issue" input_filtered_search("author:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([{ name: 'author', value: user.username }])
expect_issues_list_count(3) expect_issues_list_count(3)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched author, assignee and text' do it 'filters issues by searched author, assignee and text' do
search = "author:@#{user.username} assignee:@#{user.username} issue" input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username }
])
expect_issues_list_count(3) expect_issues_list_count(3)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched author, assignee, label, and text' do it 'filters issues by searched author, assignee, label, and text' do
search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username },
{ name: 'label', value: caps_sensitive_label.title }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched author, assignee, label, milestone and text' do it 'filters issues by searched author, assignee, label, milestone and text' do
search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username },
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'milestone', value: milestone.title }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
end end
...@@ -151,19 +167,19 @@ describe 'Filter issues', js: true, feature: true do ...@@ -151,19 +167,19 @@ describe 'Filter issues', js: true, feature: true do
describe 'filter issues by assignee' do describe 'filter issues by assignee' do
context 'only assignee' do context 'only assignee' do
it 'filters issues by searched assignee' do it 'filters issues by searched assignee' do
search = "assignee:@#{user.username}" input_filtered_search("assignee:@#{user.username}")
input_filtered_search(search)
expect_tokens([{ name: 'assignee', value: user.username }])
expect_issues_list_count(5) expect_issues_list_count(5)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'filters issues by no assignee' do it 'filters issues by no assignee' do
search = "assignee:none" input_filtered_search('assignee:none')
input_filtered_search(search)
expect_tokens([{ name: 'assignee', value: 'none' }])
expect_issues_list_count(8, 1) expect_issues_list_count(8, 1)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'filters issues by invalid assignee' do it 'filters issues by invalid assignee' do
...@@ -176,36 +192,50 @@ describe 'Filter issues', js: true, feature: true do ...@@ -176,36 +192,50 @@ describe 'Filter issues', js: true, feature: true do
end end
context 'assignee with other filters' do context 'assignee with other filters' do
let(:search_term) { 'searchTerm' }
it 'filters issues by searched assignee and text' do it 'filters issues by searched assignee and text' do
search = "assignee:@#{user.username} searchTerm" input_filtered_search("assignee:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([{ name: 'assignee', value: user.username }])
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched assignee, author and text' do it 'filters issues by searched assignee, author and text' do
search = "assignee:@#{user.username} author:@#{user.username} searchTerm" input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'assignee', value: user.username },
{ name: 'author', value: user.username }
])
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched assignee, author, label, text' do it 'filters issues by searched assignee, author, label, text' do
search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'assignee', value: user.username },
{ name: 'author', value: user.username },
{ name: 'label', value: caps_sensitive_label.title }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched assignee, author, label, milestone and text' do it 'filters issues by searched assignee, author, label, milestone and text' do
search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'assignee', value: user.username },
{ name: 'author', value: user.username },
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'milestone', value: milestone.title }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
end end
...@@ -217,21 +247,23 @@ describe 'Filter issues', js: true, feature: true do ...@@ -217,21 +247,23 @@ describe 'Filter issues', js: true, feature: true do
end end
describe 'filter issues by label' do describe 'filter issues by label' do
let(:search_term) { 'bug' }
context 'only label' do context 'only label' do
it 'filters issues by searched label' do it 'filters issues by searched label' do
search = "label:~#{bug_label.title}" input_filtered_search("label:~#{bug_label.title}")
input_filtered_search(search)
expect_tokens([{ name: 'label', value: bug_label.title }])
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'filters issues by no label' do it 'filters issues by no label' do
search = "label:none" input_filtered_search('label:none')
input_filtered_search(search)
expect_tokens([{ name: 'label', value: 'none' }])
expect_issues_list_count(9, 1) expect_issues_list_count(9, 1)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'filters issues by invalid label' do it 'filters issues by invalid label' do
...@@ -239,11 +271,14 @@ describe 'Filter issues', js: true, feature: true do ...@@ -239,11 +271,14 @@ describe 'Filter issues', js: true, feature: true do
end end
it 'filters issues by multiple labels' do it 'filters issues by multiple labels' do
search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}")
input_filtered_search(search)
expect_tokens([
{ name: 'label', value: bug_label.title },
{ name: 'label', value: caps_sensitive_label.title }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'filters issues by label containing special characters' do it 'filters issues by label containing special characters' do
...@@ -251,21 +286,20 @@ describe 'Filter issues', js: true, feature: true do ...@@ -251,21 +286,20 @@ describe 'Filter issues', js: true, feature: true do
special_issue = create(:issue, title: "Issue with special character label", project: project) special_issue = create(:issue, title: "Issue with special character label", project: project)
special_issue.labels << special_label special_issue.labels << special_label
search = "label:~#{special_label.title}" input_filtered_search("label:~#{special_label.title}")
input_filtered_search(search) expect_tokens([{ name: 'label', value: special_label.title }])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'does not show issues' do it 'does not show issues' do
new_label = create(:label, project: project, title: "new_label") new_label = create(:label, project: project, title: 'new_label')
search = "label:~#{new_label.title}" input_filtered_search("label:~#{new_label.title}")
input_filtered_search(search)
expect_tokens([{ name: 'label', value: new_label.title }])
expect_no_issues_list() expect_no_issues_list()
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
end end
...@@ -275,29 +309,29 @@ describe 'Filter issues', js: true, feature: true do ...@@ -275,29 +309,29 @@ describe 'Filter issues', js: true, feature: true do
special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project)
special_multiple_issue.labels << special_multiple_label special_multiple_issue.labels << special_multiple_label
search = "label:~'#{special_multiple_label.title}'" input_filtered_search("label:~'#{special_multiple_label.title}'")
input_filtered_search(search)
# filtered search defaults quotations to double quotes
expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }])
expect_issues_list_count(1) expect_issues_list_count(1)
# filtered search defaults quotations to double quotes expect_filtered_search_input_empty
expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"")
end end
it 'single quotes' do it 'single quotes' do
search = "label:~'#{multiple_words_label.title}'" input_filtered_search("label:~'#{multiple_words_label.title}'")
input_filtered_search(search)
expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") expect_filtered_search_input_empty
end end
it 'double quotes' do it 'double quotes' do
search = "label:~\"#{multiple_words_label.title}\"" input_filtered_search("label:~\"#{multiple_words_label.title}\"")
input_filtered_search(search)
expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'single quotes containing double quotes' do it 'single quotes containing double quotes' do
...@@ -305,11 +339,11 @@ describe 'Filter issues', js: true, feature: true do ...@@ -305,11 +339,11 @@ describe 'Filter issues', js: true, feature: true do
double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project)
double_quotes_label_issue.labels << double_quotes_label double_quotes_label_issue.labels << double_quotes_label
search = "label:~'#{double_quotes_label.title}'" input_filtered_search("label:~'#{double_quotes_label.title}'")
input_filtered_search(search)
expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'double quotes containing single quotes' do it 'double quotes containing single quotes' do
...@@ -317,86 +351,115 @@ describe 'Filter issues', js: true, feature: true do ...@@ -317,86 +351,115 @@ describe 'Filter issues', js: true, feature: true do
single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project)
single_quotes_label_issue.labels << single_quotes_label single_quotes_label_issue.labels << single_quotes_label
search = "label:~\"#{single_quotes_label.title}\"" input_filtered_search("label:~\"#{single_quotes_label.title}\"")
input_filtered_search(search)
expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
end end
context 'label with other filters' do context 'label with other filters' do
it 'filters issues by searched label and text' do it 'filters issues by searched label and text' do
search = "label:~#{caps_sensitive_label.title} bug" input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}")
input_filtered_search(search)
expect_tokens([{ name: 'label', value: caps_sensitive_label.title }])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched label, author and text' do it 'filters issues by searched label, author and text' do
search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'author', value: user.username }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched label, author, assignee and text' do it 'filters issues by searched label, author, assignee and text' do
search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched label, author, assignee, milestone and text' do it 'filters issues by searched label, author, assignee, milestone and text' do
search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username },
{ name: 'milestone', value: milestone.title }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
end end
context 'multiple labels with other filters' do context 'multiple labels with other filters' do
it 'filters issues by searched label, label2, and text' do it 'filters issues by searched label, label2, and text' do
search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'label', value: bug_label.title },
{ name: 'label', value: caps_sensitive_label.title }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched label, label2, author and text' do it 'filters issues by searched label, label2, author and text' do
search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'label', value: bug_label.title },
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'author', value: user.username }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched label, label2, author, assignee and text' do it 'filters issues by searched label, label2, author, assignee and text' do
search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'label', value: bug_label.title },
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched label, label2, author, assignee, milestone and text' do it 'filters issues by searched label, label2, author, assignee, milestone and text' do
search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'label', value: bug_label.title },
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username },
{ name: 'milestone', value: milestone.title }
])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
end end
context 'issue label clicked' do context 'issue label clicked' do
before do before do
find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click
sleep 1
end end
it 'filters' do it 'filters' do
...@@ -404,7 +467,8 @@ describe 'Filter issues', js: true, feature: true do ...@@ -404,7 +467,8 @@ describe 'Filter issues', js: true, feature: true do
end end
it 'displays in search bar' do it 'displays in search bar' do
expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_filtered_search_input_empty
end end
end end
...@@ -420,19 +484,25 @@ describe 'Filter issues', js: true, feature: true do ...@@ -420,19 +484,25 @@ describe 'Filter issues', js: true, feature: true do
it 'filters issues by searched milestone' do it 'filters issues by searched milestone' do
input_filtered_search("milestone:%#{milestone.title}") input_filtered_search("milestone:%#{milestone.title}")
expect_tokens([{ name: 'milestone', value: milestone.title }])
expect_issues_list_count(5) expect_issues_list_count(5)
expect_filtered_search_input_empty
end end
it 'filters issues by no milestone' do it 'filters issues by no milestone' do
input_filtered_search("milestone:none") input_filtered_search("milestone:none")
expect_tokens([{ name: 'milestone', value: 'none' }])
expect_issues_list_count(7, 1) expect_issues_list_count(7, 1)
expect_filtered_search_input_empty
end end
it 'filters issues by upcoming milestones' do it 'filters issues by upcoming milestones' do
input_filtered_search("milestone:upcoming") input_filtered_search("milestone:upcoming")
expect_tokens([{ name: 'milestone', value: 'upcoming' }])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input_empty
end end
it 'filters issues by invalid milestones' do it 'filters issues by invalid milestones' do
...@@ -447,55 +517,69 @@ describe 'Filter issues', js: true, feature: true do ...@@ -447,55 +517,69 @@ describe 'Filter issues', js: true, feature: true do
special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone)
search = "milestone:%#{special_milestone.title}" input_filtered_search("milestone:%#{special_milestone.title}")
input_filtered_search(search)
expect_tokens([{ name: 'milestone', value: special_milestone.title }])
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
it 'does not show issues' do it 'does not show issues' do
new_milestone = create(:milestone, title: "new", project: project) new_milestone = create(:milestone, title: "new", project: project)
search = "milestone:%#{new_milestone.title}" input_filtered_search("milestone:%#{new_milestone.title}")
input_filtered_search(search)
expect_tokens([{ name: 'milestone', value: new_milestone.title }])
expect_no_issues_list() expect_no_issues_list()
expect_filtered_search_input(search) expect_filtered_search_input_empty
end end
end end
context 'milestone with other filters' do context 'milestone with other filters' do
search_term = 'bug'
it 'filters issues by searched milestone and text' do it 'filters issues by searched milestone and text' do
search = "milestone:%#{milestone.title} bug" input_filtered_search("milestone:%#{milestone.title} #{search_term}")
input_filtered_search(search)
expect_tokens([{ name: 'milestone', value: milestone.title }])
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched milestone, author and text' do it 'filters issues by searched milestone, author and text' do
search = "milestone:%#{milestone.title} author:@#{user.username} bug" input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'milestone', value: milestone.title },
{ name: 'author', value: user.username }
])
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched milestone, author, assignee and text' do it 'filters issues by searched milestone, author, assignee and text' do
search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'milestone', value: milestone.title },
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username }
])
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
it 'filters issues by searched milestone, author, assignee, label and text' do it 'filters issues by searched milestone, author, assignee, label and text' do
search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}")
input_filtered_search(search)
expect_tokens([
{ name: 'milestone', value: milestone.title },
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username },
{ name: 'label', value: bug_label.title }
])
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input(search) expect_filtered_search_input(search_term)
end end
end end
...@@ -506,44 +590,6 @@ describe 'Filter issues', js: true, feature: true do ...@@ -506,44 +590,6 @@ describe 'Filter issues', js: true, feature: true do
end end
end end
describe 'overwrites selected filter' do
it 'changes author' do
input_filtered_search("author:@#{user.username}", submit: false)
select_search_at_index(3)
page.within '#js-dropdown-author' do
click_button user2.username
end
expect(filtered_search.value).to eq("author:@#{user2.username} ")
end
it 'changes label' do
input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false)
select_search_at_index(27)
page.within '#js-dropdown-label' do
click_button label.name
end
expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ")
end
it 'changes label correctly space is in previous label' do
input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false)
select_search_at_index(0)
page.within '#js-dropdown-label' do
click_button label.name
end
expect(filtered_search.value).to eq("label:~#{label.name} ")
end
end
describe 'filter issues by text' do describe 'filter issues by text' do
context 'only text' do context 'only text' do
it 'filters issues by searched text' do it 'filters issues by searched text' do
...@@ -605,80 +651,81 @@ describe 'Filter issues', js: true, feature: true do ...@@ -605,80 +651,81 @@ describe 'Filter issues', js: true, feature: true do
context 'searched text with other filters' do context 'searched text with other filters' do
it 'filters issues by searched text and author' do it 'filters issues by searched text and author' do
# After searching, all search terms are placed at the end
input_filtered_search("bug author:@#{user.username}") input_filtered_search("bug author:@#{user.username}")
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input("author:@#{user.username} bug") expect_filtered_search_input('bug')
end end
it 'filters issues by searched text, author and more text' do it 'filters issues by searched text, author and more text' do
input_filtered_search("bug author:@#{user.username} report") input_filtered_search("bug author:@#{user.username} report")
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} bug report") expect_filtered_search_input('bug report')
end end
it 'filters issues by searched text, author and assignee' do it 'filters issues by searched text, author and assignee' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}")
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") expect_filtered_search_input('bug')
end end
it 'filters issues by searched text, author, more text and assignee' do it 'filters issues by searched text, author, more text and assignee' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}")
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") expect_filtered_search_input('bug report')
end end
it 'filters issues by searched text, author, more text, assignee and even more text' do it 'filters issues by searched text, author, more text, assignee and even more text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with")
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") expect_filtered_search_input('bug report with')
end end
it 'filters issues by searched text, author, assignee and label' do it 'filters issues by searched text, author, assignee and label' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}")
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") expect_filtered_search_input('bug')
end end
it 'filters issues by searched text, author, text, assignee, text, label and text' do it 'filters issues by searched text, author, text, assignee, text, label and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything")
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") expect_filtered_search_input('bug report with everything')
end end
it 'filters issues by searched text, author, assignee, label and milestone' do it 'filters issues by searched text, author, assignee, label and milestone' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}")
expect_issues_list_count(2) expect_issues_list_count(2)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") expect_filtered_search_input('bug')
end end
it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you")
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") expect_filtered_search_input('bug report with everything you')
end end
it 'filters issues by searched text, author, assignee, multiple labels and milestone' do it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}")
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") expect_filtered_search_input('bug')
end end
it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought")
expect_issues_list_count(1) expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") expect_filtered_search_input('bug report with everything you thought')
end end
end end
...@@ -717,8 +764,8 @@ describe 'Filter issues', js: true, feature: true do ...@@ -717,8 +764,8 @@ describe 'Filter issues', js: true, feature: true do
before do before do
input_filtered_search('bug') input_filtered_search('bug')
# Wait for search results to load # This ensures that the search is performed
sleep 2 expect_issues_list_count(4, 1)
end end
it 'open state' do it 'open state' do
......
require 'rails_helper' require 'rails_helper'
describe 'Search bar', js: true, feature: true do describe 'Search bar', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let!(:project) { create(:empty_project) } let!(:project) { create(:empty_project) }
...@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do ...@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do
it 'selects item' do it 'selects item' do
filtered_search.native.send_keys(:down, :down, :enter) filtered_search.native.send_keys(:down, :down, :enter)
expect(filtered_search.value).to eq('author:') expect_tokens([{ name: 'author' }])
expect_filtered_search_input_empty
end end
end end
......
require 'rails_helper'
describe 'Visual tokens', js: true, feature: true do
include FilteredSearchHelpers
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_rock) { create(:user, name: 'The Rock', username: 'rock') }
let!(:milestone_nine) { create(:milestone, title: '9.0', project: project) }
let!(:milestone_ten) { create(:milestone, title: '10.0', project: project) }
let!(:label) { create(:label, project: project, title: 'abc') }
let!(:cc_label) { create(:label, project: project, title: 'Community Contribution') }
let(:filtered_search) { find('.filtered-search') }
let(:filter_author_dropdown) { find("#js-dropdown-author .filter-dropdown") }
let(:filter_assignee_dropdown) { find("#js-dropdown-assignee .filter-dropdown") }
let(:filter_milestone_dropdown) { find("#js-dropdown-milestone .filter-dropdown") }
let(:filter_label_dropdown) { find("#js-dropdown-label .filter-dropdown") }
def is_input_focused
page.evaluate_script("document.activeElement.classList.contains('filtered-search')")
end
before do
project.add_user(user, :master)
project.add_user(user_rock, :master)
login_as(user)
create(:issue, project: project)
visit namespace_project_issues_path(project.namespace, project)
end
describe 'editing author token' do
before do
input_filtered_search('author:@root assignee:none', submit: false)
first('.tokens-container .filtered-search-token').double_click
end
it 'opens author dropdown' do
expect(page).to have_css('#js-dropdown-author', visible: true)
end
it 'makes value editable' do
expect_filtered_search_input('@root')
end
it 'filters value' do
filtered_search.send_keys(:backspace)
expect(page).to have_css('#js-dropdown-author .filter-dropdown .filter-dropdown-item', count: 1)
end
it 'ends editing mode when document is clicked' do
find('#content-body').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-author', visible: false)
end
it 'ends editing mode when scroll container is clicked' do
find('.scroll-container').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-author', visible: false)
end
describe 'selecting different author from dropdown' do
before do
filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click
end
it 'changes value in visual token' do
expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}")
end
it 'moves input to the right' do
expect(is_input_focused).to eq(true)
end
end
end
describe 'editing assignee token' do
before do
input_filtered_search('assignee:@root author:none', submit: false)
first('.tokens-container .filtered-search-token').double_click
end
it 'opens assignee dropdown' do
expect(page).to have_css('#js-dropdown-assignee', visible: true)
end
it 'makes value editable' do
expect_filtered_search_input('@root')
end
it 'filters value' do
filtered_search.send_keys(:backspace)
expect(page).to have_css('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', count: 1)
end
it 'ends editing mode when document is clicked' do
find('#content-body').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-assignee', visible: false)
end
it 'ends editing mode when scroll container is clicked' do
find('.scroll-container').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-assignee', visible: false)
end
describe 'selecting static option from dropdown' do
before do
find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click
end
it 'changes value in visual token' do
expect(first('.tokens-container .filtered-search-token .value').text).to eq('none')
end
it 'moves input to the right' do
expect(is_input_focused).to eq(true)
end
end
end
describe 'editing milestone token' do
before do
input_filtered_search('milestone:%10.0 author:none', submit: false)
first('.tokens-container .filtered-search-token').double_click
first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
end
it 'opens milestone dropdown' do
expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_ten.title)).to be_visible
expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_nine.title)).to be_visible
expect(page).to have_css('#js-dropdown-milestone', visible: true)
end
it 'selects static option from dropdown' do
find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click
expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming')
expect(is_input_focused).to eq(true)
end
it 'makes value editable' do
expect_filtered_search_input('%10.0')
end
it 'filters value' do
filtered_search.send_keys(:backspace)
expect(page).to have_css('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', count: 1)
end
it 'ends editing mode when document is clicked' do
find('#content-body').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-milestone', visible: false)
end
it 'ends editing mode when scroll container is clicked' do
find('.scroll-container').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-milestone', visible: false)
end
end
describe 'editing label token' do
before do
input_filtered_search("label:~#{label.title} author:none", submit: false)
first('.tokens-container .filtered-search-token').double_click
first('#js-dropdown-label .filter-dropdown .filter-dropdown-item')
end
it 'opens label dropdown' do
expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
expect(page).to have_css('#js-dropdown-label', visible: true)
end
it 'selects option from dropdown' do
expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
find("#js-dropdown-label").find('.filter-dropdown-item', text: cc_label.title).click
expect(first('.tokens-container .filtered-search-token .value').text).to eq("~\"#{cc_label.title}\"")
expect(is_input_focused).to eq(true)
end
it 'makes value editable' do
expect_filtered_search_input("~#{label.title}")
end
it 'filters value' do
expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
filtered_search.send_keys(:backspace)
filter_label_dropdown.find('.filter-dropdown-item')
expect(page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size).to eq(1)
end
it 'ends editing mode when document is clicked' do
find('#content-body').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-label', visible: false)
end
it 'ends editing mode when scroll container is clicked' do
find('.scroll-container').click
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-label', visible: false)
end
end
describe 'add new token after editing existing token' do
before do
input_filtered_search('author:@root assignee:none', submit: false)
first('.tokens-container .filtered-search-token').double_click
filtered_search.send_keys(' ')
end
describe 'opens dropdowns' do
it 'opens hint dropdown' do
expect(page).to have_css('#js-dropdown-hint', visible: true)
end
it 'opens author dropdown' do
filtered_search.send_keys('author:')
expect(page).to have_css('#js-dropdown-author', visible: true)
end
it 'opens assignee dropdown' do
filtered_search.send_keys('assignee:')
expect(page).to have_css('#js-dropdown-assignee', visible: true)
end
it 'opens milestone dropdown' do
filtered_search.send_keys('milestone:')
expect(page).to have_css('#js-dropdown-milestone', visible: true)
end
it 'opens label dropdown' do
filtered_search.send_keys('label:')
expect(page).to have_css('#js-dropdown-label', visible: true)
end
end
describe 'creates visual tokens' do
it 'creates author token' do
filtered_search.send_keys('author:@thomas ')
token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Author')
expect(token.find('.value').text).to eq('@thomas')
end
it 'creates assignee token' do
filtered_search.send_keys('assignee:@thomas ')
token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Assignee')
expect(token.find('.value').text).to eq('@thomas')
end
it 'creates milestone token' do
filtered_search.send_keys('milestone:none ')
token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Milestone')
expect(token.find('.value').text).to eq('none')
end
it 'creates label token' do
filtered_search.send_keys('label:~Backend ')
token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Label')
expect(token.find('.value').text).to eq('~Backend')
end
end
it 'does not tokenize incomplete token' do
filtered_search.send_keys('author:')
find('#content-body').click
token = page.all('.tokens-container .js-visual-token')[1]
expect_filtered_search_input_empty
expect(token.find('.name').text).to eq('Author')
end
end
end
...@@ -70,7 +70,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do ...@@ -70,7 +70,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do
context 'filter by label enhancement and bug in issues list' do context 'filter by label enhancement and bug in issues list' do
before do before do
input_filtered_search('label:~bug label:~enhancement') input_filtered_search('label:~bug label:~enhancement ')
end end
it 'applies the filters' do it 'applies the filters' do
......
...@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project) visit_merge_requests(project)
input_filtered_search('milestone:none') input_filtered_search('milestone:none')
expect_tokens([{ name: 'milestone', value: 'none' }])
expect_filtered_search_input_empty
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
......
...@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do ...@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do
describe 'for assignee from mr#index' do describe 'for assignee from mr#index' do
let(:search_query) { "assignee:@#{user.username}" } let(:search_query) { "assignee:@#{user.username}" }
def expect_assignee_visual_tokens
expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end
before do before do
input_filtered_search(search_query) input_filtered_search(search_query)
...@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do ...@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do
context 'assignee', js: true do context 'assignee', js: true do
it 'updates to current user' do it 'updates to current user' do
expect_filtered_search_input(search_query) expect_assignee_visual_tokens()
end end
it 'does not change when closed link is clicked' do it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query) expect_assignee_visual_tokens()
end end
it 'does not change when all link is clicked' do it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query) expect_assignee_visual_tokens()
end end
end end
end end
describe 'for milestone from mr#index' do describe 'for milestone from mr#index' do
let(:search_query) { "milestone:%#{milestone.title}" } let(:search_query) { "milestone:%\"#{milestone.title}\"" }
def expect_milestone_visual_tokens
expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }])
expect_filtered_search_input_empty
end
before do before do
input_filtered_search(search_query) input_filtered_search(search_query)
...@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do ...@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do
context 'milestone', js: true do context 'milestone', js: true do
it 'updates to current milestone' do it 'updates to current milestone' do
expect_filtered_search_input(search_query) expect_milestone_visual_tokens()
end end
it 'does not change when closed link is clicked' do it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query) expect_milestone_visual_tokens()
end end
it 'does not change when all link is clicked' do it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query) expect_milestone_visual_tokens()
end end
end end
end end
...@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do ...@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do
input_filtered_search('label:none') input_filtered_search('label:none')
expect_mr_list_count(1) expect_mr_list_count(1)
expect_filtered_search_input('label:none') expect_tokens([{ name: 'label', value: 'none' }])
expect_filtered_search_input_empty
end end
it 'filters by a label' do it 'filters by a label' do
input_filtered_search("label:~#{label.title}") input_filtered_search("label:~#{label.title}")
expect_mr_list_count(0) expect_mr_list_count(0)
expect_filtered_search_input("label:~#{label.title}") expect_tokens([{ name: 'label', value: "~#{label.title}" }])
expect_filtered_search_input_empty
end end
it "filters by `won't fix` and another label" do it "filters by `won't fix` and another label" do
input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}") input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
expect_mr_list_count(0) expect_mr_list_count(0)
expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") expect_tokens([
{ name: 'label', value: "~\"#{wontfix.title}\"" },
{ name: 'label', value: "~#{label.title}" }
])
expect_filtered_search_input_empty
end end
it "filters by `won't fix` label followed by another label after page load" do it "filters by `won't fix` label followed by another label after page load" do
input_filtered_search("label:~\"#{wontfix.title}\"") input_filtered_search("label:~\"#{wontfix.title}\"")
expect_mr_list_count(0) expect_mr_list_count(0)
expect_filtered_search_input("label:~\"#{wontfix.title}\"") expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }])
expect_filtered_search_input_empty
input_filtered_search_keys(" label:~#{label.title}")
expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") input_filtered_search_keys("label:~#{label.title}")
expect_mr_list_count(0) expect_mr_list_count(0)
expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}") expect_tokens([
{ name: 'label', value: "~\"#{wontfix.title}\"" },
{ name: 'label', value: "~#{label.title}" }
])
expect_filtered_search_input_empty
end end
end end
...@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do ...@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do
input_filtered_search("assignee:@#{user.username}") input_filtered_search("assignee:@#{user.username}")
expect_mr_list_count(1) expect_mr_list_count(1)
expect_filtered_search_input("assignee:@#{user.username}") expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input_empty
input_filtered_search_keys(" label:~#{label.title}") input_filtered_search_keys("label:~#{label.title} ")
expect_mr_list_count(1) expect_mr_list_count(1)
...@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do ...@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do
end end
context 'assignee and label', js: true do context 'assignee and label', js: true do
def expect_assignee_label_visual_tokens
expect_tokens([
{ name: 'assignee', value: "@#{user.username}" },
{ name: 'label', value: "~#{label.title}" }
])
expect_filtered_search_input_empty
end
it 'updates to current assignee and label' do it 'updates to current assignee and label' do
expect_filtered_search_input(search_query) expect_assignee_label_visual_tokens()
end end
it 'does not change when closed link is clicked' do it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query) expect_assignee_label_visual_tokens()
end end
it 'does not change when all link is clicked' do it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query) expect_assignee_label_visual_tokens()
end end
end end
end end
...@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do ...@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' label:~bug') input_filtered_search_keys(' label:~bug')
expect_mr_list_count(1) expect_mr_list_count(1)
expect_tokens([{ name: 'label', value: '~bug' }])
expect_filtered_search_input('Bug')
end end
it 'filters by text and milestone' do it 'filters by text and milestone' do
...@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do ...@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' milestone:%8') input_filtered_search_keys(' milestone:%8')
expect_mr_list_count(1) expect_mr_list_count(1)
expect_tokens([{ name: 'milestone', value: '%8' }])
expect_filtered_search_input('Bug')
end end
it 'filters by text and assignee' do it 'filters by text and assignee' do
...@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do ...@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" assignee:@#{user.username}") input_filtered_search_keys(" assignee:@#{user.username}")
expect_mr_list_count(1) expect_mr_list_count(1)
expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input('Bug')
end end
it 'filters by text and author' do it 'filters by text and author' do
...@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do ...@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" author:@#{user.username}") input_filtered_search_keys(" author:@#{user.username}")
expect_mr_list_count(1) expect_mr_list_count(1)
expect_tokens([{ name: 'author', value: "@#{user.username}" }])
expect_filtered_search_input('Bug')
end end
end end
end end
...@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do ...@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do it 'filter by current user' do
visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id) visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id)
expect_filtered_search_input("assignee:@#{user.username}") expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end end
it 'filter by new user' do it 'filter by new user' do
...@@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do ...@@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do
visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id) visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id)
expect_filtered_search_input("assignee:@#{new_user.username}") expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }])
expect_filtered_search_input_empty
end end
end end
...@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do ...@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do it 'filter by current user' do
visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id) visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id)
expect_filtered_search_input("author:@#{user.username}") expect_tokens([{ name: 'author', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end end
it 'filter by new user' do it 'filter by new user' do
...@@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do ...@@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do
visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id) visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id)
expect_filtered_search_input("author:@#{new_user.username}") expect_tokens([{ name: 'author', value: "@#{new_user.username}" }])
expect_filtered_search_input_empty
end end
end end
end end
require 'rails_helper' require 'rails_helper'
feature 'Issues filter reset button', feature: true, js: true do feature 'Merge requests filter clear button', feature: true, js: true do
include FilteredSearchHelpers include FilteredSearchHelpers
include MergeRequestHelpers include MergeRequestHelpers
include WaitForAjax include WaitForAjax
...@@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do ...@@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when a milestone filter has been applied' do context 'when a milestone filter has been applied' do
it 'resets the milestone filter' do it 'resets the milestone filter' do
visit_merge_requests(project, milestone_title: milestone.title) visit_merge_requests(project, milestone_title: milestone.title)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when a label filter has been applied' do context 'when a label filter has been applied' do
it 'resets the label filter' do it 'resets the label filter' do
visit_merge_requests(project, label_name: bug.name) visit_merge_requests(project, label_name: bug.name)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when a text search has been conducted' do context 'when a text search has been conducted' do
it 'resets the text search filter' do it 'resets the text search filter' do
visit_merge_requests(project, search: 'Bug') visit_merge_requests(project, search: 'Bug')
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when author filter has been applied' do context 'when author filter has been applied' do
it 'resets the author filter' do it 'resets the author filter' do
visit_merge_requests(project, author_username: user.username) visit_merge_requests(project, author_username: user.username)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when assignee filter has been applied' do context 'when assignee filter has been applied' do
it 'resets the assignee filter' do it 'resets the assignee filter' do
visit_merge_requests(project, assignee_username: user.username) visit_merge_requests(project, assignee_username: user.username)
expect(page).to have_css(merge_request_css, count: 1) expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when all filters have been applied' do context 'when all filters have been applied' do
it 'resets all filters' do it 'clears all filters' do
visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
expect(page).to have_css(merge_request_css, count: 0) expect(page).to have_css(merge_request_css, count: 0)
expect(get_filtered_search_placeholder).to eq('')
reset_filters reset_filters
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end end
end end
context 'when no filters have been applied' do context 'when no filters have been applied' do
it 'the reset link should not be visible' do it 'the clear button should not be visible' do
visit_merge_requests(project) visit_merge_requests(project)
expect(page).to have_css(merge_request_css, count: 2) expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
expect(page).not_to have_css(clear_search_css) expect(page).not_to have_css(clear_search_css)
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe "Search", feature: true do describe "Search", feature: true do
include FilteredSearchHelpers
include WaitForAjax include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -170,7 +171,8 @@ describe "Search", feature: true do ...@@ -170,7 +171,8 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.filtered-search') expect(page).to have_selector('.filtered-search')
expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end end
it 'takes user to her issues page when issues authored is clicked' do it 'takes user to her issues page when issues authored is clicked' do
...@@ -178,7 +180,8 @@ describe "Search", feature: true do ...@@ -178,7 +180,8 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.filtered-search') expect(page).to have_selector('.filtered-search')
expect(find('.filtered-search').value).to eq("author:@#{user.username}") expect_tokens([{ name: 'author', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end end
it 'takes user to her MR page when MR assigned is clicked' do it 'takes user to her MR page when MR assigned is clicked' do
...@@ -186,7 +189,8 @@ describe "Search", feature: true do ...@@ -186,7 +189,8 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.merge-requests-holder') expect(page).to have_selector('.merge-requests-holder')
expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end end
it 'takes user to her MR page when MR authored is clicked' do it 'takes user to her MR page when MR authored is clicked' do
...@@ -194,7 +198,8 @@ describe "Search", feature: true do ...@@ -194,7 +198,8 @@ describe "Search", feature: true do
sleep 2 sleep 2
expect(page).to have_selector('.merge-requests-holder') expect(page).to have_selector('.merge-requests-holder')
expect(find('.filtered-search').value).to eq("author:@#{user.username}") expect_tokens([{ name: 'author', value: "@#{user.username}" }])
expect_filtered_search_input_empty
end end
end end
......
...@@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user'); ...@@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user');
it('should not return the double quote found in value', () => { it('should not return the double quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: { lastToken: '"johnny appleseed',
value: '"johnny appleseed',
},
}); });
expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
...@@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user'); ...@@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user');
it('should not return the single quote found in value', () => { it('should not return the single quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: { lastToken: '\'larry boy',
value: '\'larry boy',
},
}); });
expect(dropdownUser.getSearchInput()).toBe('larry boy'); expect(dropdownUser.getSearchInput()).toBe('larry boy');
......
...@@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
}); });
it('should filter without symbol', () => { it('should filter without symbol', () => {
input.value = ':roo'; input.value = 'roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
...@@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager');
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with colon', () => {
input.value = 'roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
});
describe('filters multiple word title', () => { describe('filters multiple word title', () => {
const multipleWordItem = { const multipleWordItem = {
title: 'Community Contributions', title: 'Community Contributions',
}; };
it('should filter with double quote', () => { it('should filter with double quote', () => {
input.value = 'label:"'; input.value = '"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote and symbol', () => { it('should filter with double quote and symbol', () => {
input.value = 'label:~"'; input.value = '~"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote and multiple words', () => { it('should filter with double quote and multiple words', () => {
input.value = 'label:"community con'; input.value = '"community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with double quote, symbol and multiple words', () => { it('should filter with double quote, symbol and multiple words', () => {
input.value = 'label:~"community con'; input.value = '~"community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote', () => { it('should filter with single quote', () => {
input.value = 'label:\''; input.value = '\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote and symbol', () => { it('should filter with single quote and symbol', () => {
input.value = 'label:~\''; input.value = '~\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote and multiple words', () => { it('should filter with single quote and multiple words', () => {
input.value = 'label:\'community con'; input.value = '\'community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
}); });
it('should filter with single quote, symbol and multiple words', () => { it('should filter with single quote, symbol and multiple words', () => {
input.value = 'label:~\'community con'; input.value = '~\'community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false); expect(updatedItem.droplab_hidden).toBe(false);
......
require('~/extensions/array'); require('~/extensions/array');
require('~/filtered_search/filtered_search_visual_tokens');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_dropdown_manager');
...@@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager');
} }
beforeEach(() => { beforeEach(() => {
const input = document.createElement('input'); setFixtures(`
input.classList.add('filtered-search'); <ul class="tokens-container">
document.body.appendChild(input); <li class="input-token">
}); <input class="filtered-search">
</li>
afterEach(() => { </ul>
document.querySelector('.filtered-search').outerHTML = ''; `);
}); });
describe('input has no existing value', () => { describe('input has no existing value', () => {
it('should add just tokenName', () => { it('should add just tokenName', () => {
gl.FilteredSearchDropdownManager.addWordToInput('milestone'); gl.FilteredSearchDropdownManager.addWordToInput('milestone');
expect(getInputValue()).toBe('milestone:');
const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('milestone');
expect(getInputValue()).toBe('');
}); });
it('should add tokenName and tokenValue', () => { it('should add tokenName and tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label');
let token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label');
expect(getInputValue()).toBe('');
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
expect(getInputValue()).toBe('label:none '); // We have to get that reference again
// Because gl.FilteredSearchDropdownManager deletes the previous token
token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.value').innerText).toBe('none');
expect(getInputValue()).toBe('');
}); });
}); });
...@@ -39,19 +60,40 @@ require('~/filtered_search/filtered_search_dropdown_manager'); ...@@ -39,19 +60,40 @@ require('~/filtered_search/filtered_search_dropdown_manager');
it('should be able to just add tokenName', () => { it('should be able to just add tokenName', () => {
setInputValue('a'); setInputValue('a');
gl.FilteredSearchDropdownManager.addWordToInput('author'); gl.FilteredSearchDropdownManager.addWordToInput('author');
expect(getInputValue()).toBe('author:');
const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author');
expect(getInputValue()).toBe('');
}); });
it('should replace tokenValue', () => { it('should replace tokenValue', () => {
setInputValue('author:roo'); gl.FilteredSearchDropdownManager.addWordToInput('author');
gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
expect(getInputValue()).toBe('author:@root '); setInputValue('roo');
gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author');
expect(token.querySelector('.value').innerText).toBe('@root');
expect(getInputValue()).toBe('');
}); });
it('should add tokenValues containing spaces', () => { it('should add tokenValues containing spaces', () => {
setInputValue('label:~"test'); gl.FilteredSearchDropdownManager.addWordToInput('label');
setInputValue('"test ');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
expect(getInputValue()).toBe('label:~\'"test me"\' ');
const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
expect(getInputValue()).toBe('');
}); });
}); });
}); });
......
...@@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys'); ...@@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys');
require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_dropdown_manager');
require('~/filtered_search/filtered_search_manager'); require('~/filtered_search/filtered_search_manager');
const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
(() => { (() => {
describe('Filtered Search Manager', () => { describe('Filtered Search Manager', () => {
describe('search', () => { let input;
let manager; let manager;
const defaultParams = '?scope=all&utf8=✓&state=opened'; let tokensContainer;
const placeholder = 'Search or filter results...';
function getInput() { function dispatchBackspaceEvent(element, eventType) {
return document.querySelector('.filtered-search'); const backspaceKey = 8;
const event = new Event(eventType);
event.keyCode = backspaceKey;
element.dispatchEvent(event);
}
function dispatchDeleteEvent(element, eventType) {
const deleteKey = 46;
const event = new Event(eventType);
event.keyCode = deleteKey;
element.dispatchEvent(event);
} }
beforeEach(() => { beforeEach(() => {
setFixtures(` setFixtures(`
<input type='text' class='filtered-search' /> <div class="filtered-search-input-container">
<form>
<ul class="tokens-container list-unstyled">
${FilteredSearchSpecHelper.createInputHTML(placeholder)}
</ul>
<button class="clear-search" type="button">
<i class="fa fa-times"></i>
</button>
</form>
</div>
`); `);
spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null); spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager(); manager = new gl.FilteredSearchManager();
}); });
afterEach(() => { describe('search', () => {
getInput().outerHTML = ''; const defaultParams = '?scope=all&utf8=✓&state=opened';
});
it('should search with a single word', () => { it('should search with a single word', (done) => {
getInput().value = 'searchTerm'; input.value = 'searchTerm';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`); expect(url).toEqual(`${defaultParams}&search=searchTerm`);
done();
}); });
manager.search(); manager.search();
}); });
it('should search with multiple words', () => { it('should search with multiple words', (done) => {
getInput().value = 'awesome search terms'; input.value = 'awesome search terms';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done();
}); });
manager.search(); manager.search();
}); });
it('should search with special characters', () => { it('should search with special characters', (done) => {
getInput().value = '~!@#$%^&*()_+{}:<>,.?/'; input.value = '~!@#$%^&*()_+{}:<>,.?/';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => { spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
}); });
manager.search(); manager.search();
}); });
}); });
describe('handleInputPlaceholder', () => {
it('should render placeholder when there is no input', () => {
expect(input.placeholder).toEqual(placeholder);
});
it('should not render placeholder when there is input', () => {
input.value = 'test words';
const event = new Event('input');
input.dispatchEvent(event);
expect(input.placeholder).toEqual('');
});
it('should not render placeholder when there are tokens and no input', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
);
const event = new Event('input');
input.dispatchEvent(event);
expect(input.placeholder).toEqual('');
});
});
describe('checkForBackspace', () => {
describe('tokens and no input', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
);
});
it('removes last token', () => {
spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
dispatchBackspaceEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
});
it('sets the input', () => {
spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
dispatchDeleteEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
expect(input.value).toEqual('~bug');
});
});
it('does not remove token or change input when there is existing input', () => {
spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
input.value = 'text';
dispatchDeleteEvent(input, 'keyup');
expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
expect(input.value).toEqual('text');
});
});
describe('removeSelectedToken', () => {
function getVisualTokens() {
return tokensContainer.querySelectorAll('.js-visual-token');
}
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
});
it('removes selected token when the backspace key is pressed', () => {
expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown');
expect(getVisualTokens().length).toEqual(0);
});
it('removes selected token when the delete key is pressed', () => {
expect(getVisualTokens().length).toEqual(1);
dispatchDeleteEvent(document, 'keydown');
expect(getVisualTokens().length).toEqual(0);
});
it('updates the input placeholder after removal', () => {
manager.handleInputPlaceholder();
expect(input.placeholder).toEqual('');
expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown');
expect(input.placeholder).not.toEqual('');
expect(getVisualTokens().length).toEqual(0);
});
it('updates the clear button after removal', () => {
manager.toggleClearSearchButton();
const clearButton = document.querySelector('.clear-search');
expect(clearButton.classList.contains('hidden')).toEqual(false);
expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown');
expect(clearButton.classList.contains('hidden')).toEqual(true);
expect(getVisualTokens().length).toEqual(0);
});
});
describe('unselects token', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
`);
});
it('unselects token when input is clicked', () => {
const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
expect(selectedToken.classList.contains('selected')).toEqual(true);
expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
// Click directly on input attached to document
// so that the click event will propagate properly
document.querySelector('.filtered-search').click();
expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
expect(selectedToken.classList.contains('selected')).toEqual(false);
});
it('unselects token when document.body is clicked', () => {
const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
expect(selectedToken.classList.contains('selected')).toEqual(true);
expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
document.body.click();
expect(selectedToken.classList.contains('selected')).toEqual(false);
expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
});
});
}); });
})(); })();
require('~/filtered_search/filtered_search_visual_tokens');
const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
describe('Filtered Search Visual Tokens', () => {
let tokensContainer;
beforeEach(() => {
setFixtures(`
<ul class="tokens-container">
${FilteredSearchSpecHelper.createInputHTML()}
</ul>
`);
tokensContainer = document.querySelector('.tokens-container');
});
describe('getLastVisualTokenBeforeInput', () => {
it('returns when there are no visual tokens', () => {
const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(null);
expect(isLastVisualTokenValid).toEqual(true);
});
describe('input is the last item in tokensContainer', () => {
it('returns when there is one visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
);
const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(true);
});
it('returns when there is an incomplete visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author'),
);
const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(false);
});
it('returns when there are multiple visual tokens', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`);
const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const items = document.querySelectorAll('.tokens-container .js-visual-token');
expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
expect(isLastVisualTokenValid).toEqual(true);
});
it('returns when there are multiple visual tokens and an incomplete visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
`);
const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const items = document.querySelectorAll('.tokens-container .js-visual-token');
expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
expect(isLastVisualTokenValid).toEqual(false);
});
});
describe('input is a middle item in tokensContainer', () => {
it('returns last token before input', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`);
const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(true);
});
it('returns last partial token before input', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`);
const { lastVisualToken, isLastVisualTokenValid }
= gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(false);
});
});
});
describe('unselectTokens', () => {
it('does nothing when there are no tokens', () => {
const beforeHTML = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.unselectTokens();
expect(tokensContainer.innerHTML).toEqual(beforeHTML);
});
it('removes the selected class from buttons', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@author')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '%123', true)}
`);
const selected = tokensContainer.querySelector('.js-visual-token .selected');
expect(selected.classList.contains('selected')).toEqual(true);
gl.FilteredSearchVisualTokens.unselectTokens();
expect(selected.classList.contains('selected')).toEqual(false);
});
});
describe('selectToken', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
`);
});
it('removes the selected class if it has selected class', () => {
const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
firstTokenButton.classList.add('selected');
gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
expect(firstTokenButton.classList.contains('selected')).toEqual(false);
});
describe('has no selected class', () => {
it('adds selected class', () => {
const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
expect(firstTokenButton.classList.contains('selected')).toEqual(true);
});
it('removes selected class from other tokens', () => {
const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable');
tokenButtons[1].classList.add('selected');
gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]);
expect(tokenButtons[0].classList.contains('selected')).toEqual(true);
expect(tokenButtons[1].classList.contains('selected')).toEqual(false);
});
});
});
describe('removeSelectedToken', () => {
it('does not remove when there are no selected tokens', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
);
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
gl.FilteredSearchVisualTokens.removeSelectedToken();
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
});
it('removes selected token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
);
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
gl.FilteredSearchVisualTokens.removeSelectedToken();
expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null);
});
});
describe('createVisualTokenElementHTML', () => {
let tokenElement;
beforeEach(() => {
setFixtures(`
<div class="test-area">
${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()}
</div>
`);
tokenElement = document.querySelector('.test-area').firstElementChild;
});
it('contains name div', () => {
expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
});
it('contains value div', () => {
expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
});
it('contains selectable class', () => {
expect(tokenElement.classList.contains('selectable')).toEqual(true);
});
it('contains button role', () => {
expect(tokenElement.getAttribute('role')).toEqual('button');
});
});
describe('addVisualTokenElement', () => {
it('renders search visual tokens', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true);
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-term')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('search term');
expect(token.querySelector('.value')).toEqual(null);
});
it('renders filter visual token name', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('milestone');
expect(token.querySelector('.value')).toEqual(null);
});
it('renders filter visual token name and value', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('label');
expect(token.querySelector('.value').innerText).toEqual('Frontend');
});
it('inserts visual token before input', () => {
tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root'));
gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
const tokens = tokensContainer.querySelectorAll('.js-visual-token');
const labelToken = tokens[0];
const assigneeToken = tokens[1];
expect(labelToken.classList.contains('filtered-search-token')).toEqual(true);
expect(labelToken.querySelector('.name').innerText).toEqual('label');
expect(labelToken.querySelector('.value').innerText).toEqual('Frontend');
expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true);
expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee');
expect(assigneeToken.querySelector('.value').innerText).toEqual('@root');
});
});
describe('addValueToPreviousVisualTokenElement', () => {
it('does not add when previous visual token element has no value', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root'),
);
const original = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
expect(original).toEqual(tokensContainer.innerHTML);
});
it('does not add when previous visual token element is a search', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
`);
const original = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
expect(original).toEqual(tokensContainer.innerHTML);
});
it('adds value to previous visual filter token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label'),
);
const original = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
const updatedToken = tokensContainer.querySelector('.js-visual-token');
expect(updatedToken.querySelector('.name').innerText).toEqual('label');
expect(updatedToken.querySelector('.value').innerText).toEqual('value');
expect(original).not.toEqual(tokensContainer.innerHTML);
});
});
describe('addFilterVisualToken', () => {
it('creates visual token with just tokenName', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('milestone');
expect(token.querySelector('.value')).toEqual(null);
});
it('creates visual token with just tokenValue', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('milestone');
expect(token.querySelector('.value').innerText).toEqual('%8.17');
});
it('creates full visual token', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('assignee');
expect(token.querySelector('.value').innerText).toEqual('@john');
});
});
describe('addSearchVisualToken', () => {
it('creates search visual token', () => {
gl.FilteredSearchVisualTokens.addSearchVisualToken('search term');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-term')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('search term');
expect(token.querySelector('.value')).toEqual(null);
});
it('appends to previous search visual token if previous token was a search token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
`);
gl.FilteredSearchVisualTokens.addSearchVisualToken('append this');
const token = tokensContainer.querySelector('.filtered-search-term');
expect(token.querySelector('.name').innerText).toEqual('search term append this');
expect(token.querySelector('.value')).toEqual(null);
});
});
describe('getLastTokenPartial', () => {
it('should get last token value', () => {
const value = '~bug';
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value),
);
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value);
});
it('should get last token name if there is no value', () => {
const name = 'assignee';
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name),
);
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name);
});
it('should return empty when there are no tokens', () => {
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual('');
});
});
describe('removeLastTokenPartial', () => {
it('should remove the last token value if it exists', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"'),
);
expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null);
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null);
});
it('should remove the last token name if there is no value', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone'),
);
expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null);
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null);
});
it('should not remove anything when there are no tokens', () => {
const html = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
expect(tokensContainer.innerHTML).toEqual(html);
});
});
describe('tokenizeInput', () => {
it('does not do anything if there is no input', () => {
const original = tokensContainer.innerHTML;
gl.FilteredSearchVisualTokens.tokenizeInput();
expect(tokensContainer.innerHTML).toEqual(original);
});
it('adds search visual token if previous visual token is valid', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('assignee', 'none'),
);
const input = document.querySelector('.filtered-search');
input.value = 'some value';
gl.FilteredSearchVisualTokens.tokenizeInput();
const newToken = tokensContainer.querySelector('.filtered-search-term');
expect(input.value).toEqual('');
expect(newToken.querySelector('.name').innerText).toEqual('some value');
expect(newToken.querySelector('.value')).toEqual(null);
});
it('adds value to previous visual token element if previous visual token is invalid', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee'),
);
const input = document.querySelector('.filtered-search');
input.value = '@john';
gl.FilteredSearchVisualTokens.tokenizeInput();
const updatedToken = tokensContainer.querySelector('.filtered-search-token');
expect(input.value).toEqual('');
expect(updatedToken.querySelector('.name').innerText).toEqual('assignee');
expect(updatedToken.querySelector('.value').innerText).toEqual('@john');
});
});
describe('editToken', () => {
let input;
let token;
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')}
`);
input = document.querySelector('.filtered-search');
token = document.querySelector('.js-visual-token');
});
it('tokenize\'s existing input', () => {
input.value = 'some text';
spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
gl.FilteredSearchVisualTokens.editToken(token);
expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
expect(input.value).not.toEqual('some text');
});
it('moves input to the token position', () => {
expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null);
gl.FilteredSearchVisualTokens.editToken(token);
expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null);
expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null);
});
it('input contains the visual token value', () => {
gl.FilteredSearchVisualTokens.editToken(token);
expect(input.value).toEqual('none');
});
describe('selected token is a search term token', () => {
beforeEach(() => {
token = document.querySelector('.filtered-search-term');
});
it('token is removed', () => {
expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null);
gl.FilteredSearchVisualTokens.editToken(token);
expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null);
});
it('input has the same value as removed token', () => {
expect(input.value).toEqual('');
gl.FilteredSearchVisualTokens.editToken(token);
expect(input.value).toEqual('search');
});
});
});
describe('moveInputTotheRight', () => {
it('does nothing if the input is already the right most element', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'),
);
spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough();
gl.FilteredSearchVisualTokens.moveInputToTheRight();
expect(gl.FilteredSearchVisualTokens.tokenizeInput).not.toHaveBeenCalled();
expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
});
it('tokenize\'s input', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`;
document.querySelector('.filtered-search').value = 'none';
gl.FilteredSearchVisualTokens.moveInputToTheRight();
const value = tokensContainer.querySelector('.js-visual-token .value');
expect(value.innerText).toEqual('none');
});
it('converts input into search term token if last token is valid', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`;
document.querySelector('.filtered-search').value = 'test';
gl.FilteredSearchVisualTokens.moveInputToTheRight();
const searchValue = tokensContainer.querySelector('.filtered-search-term .name');
expect(searchValue.innerText).toEqual('test');
});
it('moves the input to the right most element', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`;
gl.FilteredSearchVisualTokens.moveInputToTheRight();
expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null);
});
});
});
class FilteredSearchSpecHelper {
static createFilterVisualTokenHTML(name, value, isSelected) {
return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML;
}
static createFilterVisualToken(name, value, isSelected = false) {
const li = document.createElement('li');
li.classList.add('js-visual-token', 'filtered-search-token');
li.innerHTML = `
<div class="selectable ${isSelected ? 'selected' : ''}" role="button">
<div class="name">${name}</div>
<div class="value">${value}</div>
</div>
`;
return li;
}
static createNameFilterVisualTokenHTML(name) {
return `
<li class="js-visual-token filtered-search-token">
<div class="name">${name}</div>
</li>
`;
}
static createSearchVisualTokenHTML(name) {
return `
<li class="js-visual-token filtered-search-term">
<div class="name">${name}</div>
</li>
`;
}
static createInputHTML(placeholder = '') {
return `
<li class="input-token">
<input type='text' class='filtered-search' placeholder='${placeholder}' />
</li>
`;
}
static createTokensContainerHTML(html, inputPlaceholder) {
return `
${html}
${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)}
`;
}
}
module.exports = FilteredSearchSpecHelper;
...@@ -3,16 +3,20 @@ module FilteredSearchHelpers ...@@ -3,16 +3,20 @@ module FilteredSearchHelpers
page.find('.filtered-search') page.find('.filtered-search')
end end
# Enables input to be set (similar to copy and paste)
def input_filtered_search(search_term, submit: true) def input_filtered_search(search_term, submit: true)
filtered_search.set(search_term) # Add an extra space to engage visual tokens
filtered_search.set("#{search_term} ")
if submit if submit
filtered_search.send_keys(:enter) filtered_search.send_keys(:enter)
end end
end end
# Enables input to be added character by character
def input_filtered_search_keys(search_term) def input_filtered_search_keys(search_term)
filtered_search.send_keys(search_term) # Add an extra space to engage visual tokens
filtered_search.send_keys("#{search_term} ")
filtered_search.send_keys(:enter) filtered_search.send_keys(:enter)
end end
...@@ -34,4 +38,32 @@ module FilteredSearchHelpers ...@@ -34,4 +38,32 @@ module FilteredSearchHelpers
# This ensures the dropdown is shown # This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading') expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
end end
def expect_filtered_search_input_empty
expect(find('.filtered-search').value).to eq('')
end
# Iterates through each visual token inside
# .tokens-container to make sure the correct names and values are rendered
def expect_tokens(tokens)
page.find '.filtered-search-input-container .tokens-container' do
page.all(:css, '.tokens-container li').each_with_index do |el, index|
token_name = tokens[index][:name]
token_value = tokens[index][:value]
expect(el.find('.name')).to have_content(token_name)
if token_value
expect(el.find('.value')).to have_content(token_value)
end
end
end
end
def default_placeholder
'Search or filter results...'
end
def get_filtered_search_placeholder
find('.filtered-search')['placeholder']
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment