Add filtered search visual tokens

This commit is contained in:
Clement Ho 2017-01-30 16:53:18 -06:00
parent b5cb1115f4
commit f44fb5cfd0
32 changed files with 2251 additions and 383 deletions

View File

@ -37,11 +37,14 @@ require('../window')(function(w){
}
}
self.hook.list[config.method].call(self.hook.list, data);
if (!self.destroyed) {
self.hook.list[config.method].call(self.hook.list, data);
}
},
init: function init(hook) {
var self = this;
self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.droplabAjax;
this.hook = hook;
@ -79,6 +82,7 @@ require('../window')(function(w){
destroy: function() {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
this.destroyed = true;
if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}

View File

@ -28,6 +28,23 @@ require('./filtered_search_dropdown');
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
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(':', ''));
}
this.dismissDropdown();
@ -39,7 +56,7 @@ require('./filtered_search_dropdown');
renderContent() {
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;
if (icon && hint && tag) {
dropdownData.push({

View File

@ -39,7 +39,12 @@ require('./filtered_search_dropdown');
getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input);
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
// with multiple words

View File

@ -22,38 +22,40 @@
static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input);
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
const searchInput = gl.DropdownUtils.getSearchInput(input);
if (lastToken !== searchToken) {
const title = updatedItem.title.toLowerCase();
let value = lastToken.value.toLowerCase();
const title = updatedItem.title.toLowerCase();
let value = searchInput.toLowerCase();
let symbol = '';
// Removes the first character if it is a quotation so that we can search
// with multiple words
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
value = value.slice(1);
}
// Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
} else {
updatedItem.droplab_hidden = false;
// 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
// with multiple words
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
value = value.slice(1);
}
// Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${symbol}${value}`) !== -1;
updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
return updatedItem;
}
static filterHint(input, item) {
const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input);
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
const searchInput = gl.DropdownUtils.getSearchInput(input);
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(searchInput);
lastToken = lastToken.key || lastToken || '';
if (!lastToken || query.split('').last() === ' ') {
if (!lastToken || searchInput.split('').last() === ' ') {
updatedItem.droplab_hidden = false;
} else if (lastToken) {
const split = lastToken.split(':');
@ -70,13 +72,40 @@
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
}
// Return boolean based on whether it was set
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) {
const inputValue = filteredSearchInput.value;
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);

View File

@ -7,3 +7,4 @@ require('./filtered_search_dropdown');
require('./filtered_search_manager');
require('./filtered_search_token_keys');
require('./filtered_search_tokenizer');
require('./filtered_search_visual_tokens');

View File

@ -35,7 +35,7 @@
if (!dataValueSet) {
const value = getValueFunction(selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.dismissDropdown();

View File

@ -58,35 +58,15 @@
};
}
static addWordToInput(tokenName, tokenValue = '') {
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = document.querySelector('.filtered-search');
const inputValue = input.value;
const word = `${tokenName}:${tokenValue}`;
// Get the string to replace
let newCaretPosition = input.selectionStart;
const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
input.value = '';
input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
// 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;
if (clicked) {
gl.FilteredSearchVisualTokens.moveInputToTheRight();
}
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() {
@ -94,19 +74,14 @@
}
updateDropdownOffset(key) {
if (!this.font) {
this.font = window.getComputedStyle(this.filteredSearchInput).font;
}
// Always align dropdown with the input field
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 :
this.mapping[key].element.clientWidth;
const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container
const offsetMaxWidth = document.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
}
@ -164,8 +139,8 @@
}
setDropdown() {
const { lastToken, searchToken } = this.tokenizer
.processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
const query = gl.DropdownUtils.getSearchQuery();
const { lastToken, searchToken } = this.tokenizer.processTokens(query);
if (this.currentDropdown) {
this.updateCurrentDropdownOffset();

View File

@ -3,6 +3,7 @@
constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search');
this.tokensContainer = document.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (this.filteredSearchInput) {
@ -27,36 +28,61 @@
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.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.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', FilteredSearchManager.editToken);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
}
unbindEvents() {
this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', FilteredSearchManager.editToken);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
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
this.dropdownManager.updateCurrentDropdownOffset();
}
@ -86,11 +112,67 @@
}
}
toggleClearSearchButton(e) {
if (e.target.value) {
this.clearSearchButton.classList.remove('hidden');
} else {
this.clearSearchButton.classList.add('hidden');
static selectToken(e) {
const button = e.target.closest('.selectable');
if (button) {
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 @@
e.preventDefault();
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.handleInputPlaceholder();
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) {
e.preventDefault();
this.search();
@ -111,7 +249,7 @@
loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams();
const inputValues = [];
let hasFilteredSearch = false;
params.forEach((p) => {
const split = p.split('=');
@ -122,7 +260,8 @@
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) {
inputValues.push(`${condition.tokenKey}:${condition.value}`);
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
} else {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
@ -140,34 +279,37 @@
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') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
inputValues.push(`assignee:@${usernameParams[id]}`);
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
inputValues.push(`author:@${usernameParams[id]}`);
hasFilteredSearch = true;
gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'search') {
inputValues.push(sanitizedValue);
hasFilteredSearch = true;
this.filteredSearchInput.value = sanitizedValue;
}
}
});
// Trim the last space value
this.filteredSearchInput.value = inputValues.join(' ');
if (inputValues.length > 0) {
if (hasFilteredSearch) {
this.clearSearchButton.classList.remove('hidden');
this.handleInputPlaceholder();
}
}
search() {
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';
paths.push(`state=${currentState}`);

View File

@ -0,0 +1,200 @@
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;

View File

@ -64,6 +64,89 @@
-webkit-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 {
@ -71,6 +154,9 @@
display: flex;
position: relative;
width: 100%;
border: 1px solid $border-color;
background-color: $white-light;
max-width: 87%;
@media (max-width: $screen-xs-min) {
-webkit-flex: 1 1 100%;
@ -87,12 +173,22 @@
}
.form-control {
padding-left: 25px;
position: relative;
min-width: 200px;
padding-left: 0;
padding-right: 25px;
border-color: transparent;
&:focus ~ .fa-filter {
color: $common-gray-dark;
}
&:focus,
&:hover {
outline: none;
border-color: transparent;
box-shadow: none;
}
}
.fa-filter {
@ -109,12 +205,13 @@
.clear-search {
width: 35px;
background-color: transparent;
background-color: $white-light;
border: none;
position: absolute;
right: 0;
height: 100%;
outline: none;
z-index: 1;
&:hover .fa-times {
color: $common-gray-dark;

View File

@ -540,3 +540,12 @@ Pipeline Graph
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
$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;

View File

@ -11,10 +11,13 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
%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')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
.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) }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-action' => 'submit' }

View File

@ -1,6 +1,9 @@
require 'rails_helper'
describe 'Dropdown assignee', :feature, :js do
include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
@ -133,7 +136,8 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee(user_jacob.name)
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
it 'fills in the assignee username when the assignee has been filtered' do
@ -141,14 +145,16 @@ describe 'Dropdown assignee', :feature, :js do
click_assignee(user.name)
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
it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
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

View File

@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Dropdown author', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do
click_author(user_jacob.name)
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
it 'fills in the author username when the author has been filtered' do
click_author(user.name)
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

View File

@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Dropdown hint', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@ -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-author', visible: true)
expect(filtered_search.value).to eq('author:')
expect_tokens([{ name: 'author' }])
expect_filtered_search_input_empty
end
it 'opens the assignee dropdown when you click on assignee' 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-assignee', visible: true)
expect(filtered_search.value).to eq('assignee:')
expect_tokens([{ name: 'assignee' }])
expect_filtered_search_input_empty
end
it 'opens the milestone dropdown when you click on milestone' 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-milestone', visible: true)
expect(filtered_search.value).to eq('milestone:')
expect_tokens([{ name: 'milestone' }])
expect_filtered_search_input_empty
end
it 'opens the label dropdown when you click on label' 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-label', visible: true)
expect(filtered_search.value).to eq('label:')
expect_tokens([{ name: 'label' }])
expect_filtered_search_input_empty
end
end
@ -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-author', visible: true)
expect(filtered_search.value).to eq('author:')
expect_tokens([{ name: 'author' }])
expect_filtered_search_input_empty
end
it 'opens the assignee dropdown when you click on assignee' 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-assignee', visible: true)
expect(filtered_search.value).to eq('assignee:')
expect_tokens([{ name: 'assignee' }])
expect_filtered_search_input_empty
end
it 'opens the milestone dropdown when you click on milestone' 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-milestone', visible: true)
expect(filtered_search.value).to eq('milestone:')
expect_tokens([{ name: 'milestone' }])
expect_filtered_search_input_empty
end
it 'opens the label dropdown when you click on label' 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-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

View File

@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do
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
@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do
end
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: uppercase_label.title)).to be_visible
@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do
clear_search_field
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: uppercase_label.title)).to be_visible
@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
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
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
click_label(bug_label.title)
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
it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title)
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
it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title)
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
it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title)
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
it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title)
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
it 'fills in the label name with special characters' do
click_label(special_label.title)
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
it 'selects `no label`' do
find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
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

View File

@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Dropdown milestone', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@ -127,7 +128,8 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title)
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
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
click_milestone(milestone.title)
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
it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title)
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
it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title)
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
it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title)
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
it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title)
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
it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title)
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
it 'selects `no milestone`' do
click_static_milestone('No Milestone')
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
it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming')
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

View File

@ -1,4 +1,4 @@
require 'rails_helper'
require 'spec_helper'
describe 'Filter issues', js: true, feature: true do
include FilteredSearchHelpers
@ -97,7 +97,9 @@ describe 'Filter issues', js: true, feature: true do
it 'filters issues by searched author' do
input_filtered_search("author:@#{user.username}")
expect_tokens([{ name: 'author', value: user.username }])
expect_issues_list_count(5)
expect_filtered_search_input_empty
end
it 'filters issues by invalid author' do
@ -110,36 +112,50 @@ describe 'Filter issues', js: true, feature: true do
end
context 'author with other filters' do
it 'filters issues by searched author and text' do
search = "author:@#{user.username} issue"
input_filtered_search(search)
search_term = 'issue'
it 'filters issues by searched author and text' do
input_filtered_search("author:@#{user.username} #{search_term}")
expect_tokens([{ name: 'author', value: user.username }])
expect_issues_list_count(3)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
it 'filters issues by searched author, assignee and text' do
search = "author:@#{user.username} assignee:@#{user.username} issue"
input_filtered_search(search)
input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}")
expect_tokens([
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username }
])
expect_issues_list_count(3)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
end
@ -151,19 +167,19 @@ describe 'Filter issues', js: true, feature: true do
describe 'filter issues by assignee' do
context 'only assignee' do
it 'filters issues by searched assignee' do
search = "assignee:@#{user.username}"
input_filtered_search(search)
input_filtered_search("assignee:@#{user.username}")
expect_tokens([{ name: 'assignee', value: user.username }])
expect_issues_list_count(5)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
it 'filters issues by no assignee' do
search = "assignee:none"
input_filtered_search(search)
input_filtered_search('assignee:none')
expect_tokens([{ name: 'assignee', value: 'none' }])
expect_issues_list_count(8, 1)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
it 'filters issues by invalid assignee' do
@ -176,36 +192,50 @@ describe 'Filter issues', js: true, feature: true do
end
context 'assignee with other filters' do
it 'filters issues by searched assignee and text' do
search = "assignee:@#{user.username} searchTerm"
input_filtered_search(search)
let(:search_term) { 'searchTerm' }
it 'filters issues by searched assignee and text' do
input_filtered_search("assignee:@#{user.username} #{search_term}")
expect_tokens([{ name: 'assignee', value: user.username }])
expect_issues_list_count(2)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
it 'filters issues by searched assignee, author and text' do
search = "assignee:@#{user.username} author:@#{user.username} searchTerm"
input_filtered_search(search)
input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}")
expect_tokens([
{ name: 'assignee', value: user.username },
{ name: 'author', value: user.username }
])
expect_issues_list_count(2)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
end
@ -217,21 +247,23 @@ describe 'Filter issues', js: true, feature: true do
end
describe 'filter issues by label' do
let(:search_term) { 'bug' }
context 'only label' do
it 'filters issues by searched label' do
search = "label:~#{bug_label.title}"
input_filtered_search(search)
input_filtered_search("label:~#{bug_label.title}")
expect_tokens([{ name: 'label', value: bug_label.title }])
expect_issues_list_count(2)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
it 'filters issues by no label' do
search = "label:none"
input_filtered_search(search)
input_filtered_search('label:none')
expect_tokens([{ name: 'label', value: 'none' }])
expect_issues_list_count(9, 1)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
it 'filters issues by invalid label' do
@ -239,11 +271,14 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by multiple labels' do
search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}"
input_filtered_search(search)
input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}")
expect_tokens([
{ name: 'label', value: bug_label.title },
{ name: 'label', value: caps_sensitive_label.title }
])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
it 'filters issues by label containing special characters' 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.labels << special_label
search = "label:~#{special_label.title}"
input_filtered_search(search)
input_filtered_search("label:~#{special_label.title}")
expect_tokens([{ name: 'label', value: special_label.title }])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
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(search)
input_filtered_search("label:~#{new_label.title}")
expect_tokens([{ name: 'label', value: new_label.title }])
expect_no_issues_list()
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
end
@ -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.labels << special_multiple_label
search = "label:~'#{special_multiple_label.title}'"
input_filtered_search(search)
expect_issues_list_count(1)
input_filtered_search("label:~'#{special_multiple_label.title}'")
# filtered search defaults quotations to double quotes
expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"")
expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
it 'single quotes' do
search = "label:~'#{multiple_words_label.title}'"
input_filtered_search(search)
input_filtered_search("label:~'#{multiple_words_label.title}'")
expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1)
expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"")
expect_filtered_search_input_empty
end
it 'double quotes' do
search = "label:~\"#{multiple_words_label.title}\""
input_filtered_search(search)
input_filtered_search("label:~\"#{multiple_words_label.title}\"")
expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
it 'single quotes containing double quotes' 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.labels << double_quotes_label
search = "label:~'#{double_quotes_label.title}'"
input_filtered_search(search)
input_filtered_search("label:~'#{double_quotes_label.title}'")
expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
it 'double quotes containing single quotes' 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.labels << single_quotes_label
search = "label:~\"#{single_quotes_label.title}\""
input_filtered_search(search)
input_filtered_search("label:~\"#{single_quotes_label.title}\"")
expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
end
context 'label with other filters' do
it 'filters issues by searched label and text' do
search = "label:~#{caps_sensitive_label.title} bug"
input_filtered_search(search)
input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}")
expect_tokens([{ name: 'label', value: caps_sensitive_label.title }])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, author and text' do
search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
input_filtered_search(search)
input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
expect_tokens([
{ name: 'label', value: caps_sensitive_label.title },
{ name: 'author', value: user.username }
])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
end
context 'multiple labels with other filters' 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(search)
input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}")
expect_tokens([
{ name: 'label', value: bug_label.title },
{ name: 'label', value: caps_sensitive_label.title }
])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
end
context 'issue label clicked' do
before do
find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click
sleep 1
end
it 'filters' do
@ -404,7 +467,8 @@ describe 'Filter issues', js: true, feature: true do
end
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
@ -420,19 +484,25 @@ describe 'Filter issues', js: true, feature: true do
it 'filters issues by searched milestone' do
input_filtered_search("milestone:%#{milestone.title}")
expect_tokens([{ name: 'milestone', value: milestone.title }])
expect_issues_list_count(5)
expect_filtered_search_input_empty
end
it 'filters issues by no milestone' do
input_filtered_search("milestone:none")
expect_tokens([{ name: 'milestone', value: 'none' }])
expect_issues_list_count(7, 1)
expect_filtered_search_input_empty
end
it 'filters issues by upcoming milestones' do
input_filtered_search("milestone:upcoming")
expect_tokens([{ name: 'milestone', value: 'upcoming' }])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
it 'filters issues by invalid milestones' do
@ -447,55 +517,69 @@ describe 'Filter issues', js: true, feature: true do
special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone)
search = "milestone:%#{special_milestone.title}"
input_filtered_search(search)
input_filtered_search("milestone:%#{special_milestone.title}")
expect_tokens([{ name: 'milestone', value: special_milestone.title }])
expect_issues_list_count(1)
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
it 'does not show issues' do
new_milestone = create(:milestone, title: "new", project: project)
search = "milestone:%#{new_milestone.title}"
input_filtered_search(search)
input_filtered_search("milestone:%#{new_milestone.title}")
expect_tokens([{ name: 'milestone', value: new_milestone.title }])
expect_no_issues_list()
expect_filtered_search_input(search)
expect_filtered_search_input_empty
end
end
context 'milestone with other filters' do
it 'filters issues by searched milestone and text' do
search = "milestone:%#{milestone.title} bug"
input_filtered_search(search)
search_term = 'bug'
it 'filters issues by searched milestone and text' do
input_filtered_search("milestone:%#{milestone.title} #{search_term}")
expect_tokens([{ name: 'milestone', value: milestone.title }])
expect_issues_list_count(2)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
it 'filters issues by searched milestone, author and text' do
search = "milestone:%#{milestone.title} author:@#{user.username} bug"
input_filtered_search(search)
input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}")
expect_tokens([
{ name: 'milestone', value: milestone.title },
{ name: 'author', value: user.username }
])
expect_issues_list_count(2)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
expect_tokens([
{ name: 'milestone', value: milestone.title },
{ name: 'author', value: user.username },
{ name: 'assignee', value: user.username }
])
expect_issues_list_count(2)
expect_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
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(search)
input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}")
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_filtered_search_input(search)
expect_filtered_search_input(search_term)
end
end
@ -506,44 +590,6 @@ describe 'Filter issues', js: true, feature: true do
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
context 'only text' do
it 'filters issues by searched text' do
@ -605,80 +651,81 @@ describe 'Filter issues', js: true, feature: true do
context 'searched text with other filters' 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}")
expect_issues_list_count(2)
expect_filtered_search_input("author:@#{user.username} bug")
expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author and more text' do
input_filtered_search("bug author:@#{user.username} report")
expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} bug report")
expect_filtered_search_input('bug report')
end
it 'filters issues by searched text, author and assignee' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}")
expect_issues_list_count(2)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug")
expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, more text and assignee' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}")
expect_issues_list_count(1)
expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report")
expect_filtered_search_input('bug report')
end
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")
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
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}")
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
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")
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
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}")
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
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")
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
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}")
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
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")
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
@ -717,8 +764,8 @@ describe 'Filter issues', js: true, feature: true do
before do
input_filtered_search('bug')
# Wait for search results to load
sleep 2
# This ensures that the search is performed
expect_issues_list_count(4, 1)
end
it 'open state' do

View File

@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Search bar', js: true, feature: true do
include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do
it 'selects item' do
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

View File

@ -0,0 +1,306 @@
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

View File

@ -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
before do
input_filtered_search('label:~bug label:~enhancement')
input_filtered_search('label:~bug label:~enhancement ')
end
it 'applies the filters' do

View File

@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
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_css('.merge-request', count: 1)
end

View File

@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do
describe 'for assignee from mr#index' do
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
input_filtered_search(search_query)
@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do
context 'assignee', js: true do
it 'updates to current user' do
expect_filtered_search_input(search_query)
expect_assignee_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query)
expect_assignee_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query)
expect_assignee_visual_tokens()
end
end
end
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
input_filtered_search(search_query)
@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do
context 'milestone', js: true do
it 'updates to current milestone' do
expect_filtered_search_input(search_query)
expect_milestone_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query)
expect_milestone_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query)
expect_milestone_visual_tokens()
end
end
end
@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do
input_filtered_search('label:none')
expect_mr_list_count(1)
expect_filtered_search_input('label:none')
expect_tokens([{ name: 'label', value: 'none' }])
expect_filtered_search_input_empty
end
it 'filters by a label' do
input_filtered_search("label:~#{label.title}")
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
it "filters by `won't fix` and another label" do
input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
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
it "filters by `won't fix` label followed by another label after page load" do
input_filtered_search("label:~\"#{wontfix.title}\"")
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_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
@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do
input_filtered_search("assignee:@#{user.username}")
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)
@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do
end
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
expect_filtered_search_input(search_query)
expect_assignee_label_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect_filtered_search_input(search_query)
expect_assignee_label_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect_filtered_search_input(search_query)
expect_assignee_label_visual_tokens()
end
end
end
@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' label:~bug')
expect_mr_list_count(1)
expect_tokens([{ name: 'label', value: '~bug' }])
expect_filtered_search_input('Bug')
end
it 'filters by text and milestone' do
@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' milestone:%8')
expect_mr_list_count(1)
expect_tokens([{ name: 'milestone', value: '%8' }])
expect_filtered_search_input('Bug')
end
it 'filters by text and assignee' do
@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" assignee:@#{user.username}")
expect_mr_list_count(1)
expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
expect_filtered_search_input('Bug')
end
it 'filters by text and author' do
@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" author:@#{user.username}")
expect_mr_list_count(1)
expect_tokens([{ name: 'author', value: "@#{user.username}" }])
expect_filtered_search_input('Bug')
end
end
end
@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do
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
it 'filter by new user' 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)
expect_filtered_search_input("assignee:@#{new_user.username}")
expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }])
expect_filtered_search_input_empty
end
end
@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do
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
it 'filter by new user' 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)
expect_filtered_search_input("author:@#{new_user.username}")
expect_tokens([{ name: 'author', value: "@#{new_user.username}" }])
expect_filtered_search_input_empty
end
end
end

View File

@ -1,6 +1,6 @@
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 MergeRequestHelpers
include WaitForAjax
@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when a milestone filter has been applied' do
it 'resets the milestone filter' do
visit_merge_requests(project, milestone_title: milestone.title)
expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters
expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when a label filter has been applied' do
it 'resets the label filter' do
visit_merge_requests(project, label_name: bug.name)
expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters
expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when a text search has been conducted' do
it 'resets the text search filter' do
visit_merge_requests(project, search: 'Bug')
expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters
expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when author filter has been applied' do
it 'resets the author filter' do
visit_merge_requests(project, author_username: user.username)
expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters
expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when assignee filter has been applied' do
it 'resets the assignee filter' do
visit_merge_requests(project, assignee_username: user.username)
expect(page).to have_css(merge_request_css, count: 1)
expect(get_filtered_search_placeholder).to eq('')
reset_filters
expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
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')
expect(page).to have_css(merge_request_css, count: 0)
expect(get_filtered_search_placeholder).to eq('')
reset_filters
expect(page).to have_css(merge_request_css, count: 2)
expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
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)
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)
end
end

View File

@ -1,6 +1,7 @@
require 'spec_helper'
describe "Search", feature: true do
include FilteredSearchHelpers
include WaitForAjax
let(:user) { create(:user) }
@ -170,7 +171,8 @@ describe "Search", feature: true do
sleep 2
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
it 'takes user to her issues page when issues authored is clicked' do
@ -178,7 +180,8 @@ describe "Search", feature: true do
sleep 2
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
it 'takes user to her MR page when MR assigned is clicked' do
@ -186,7 +189,8 @@ describe "Search", feature: true do
sleep 2
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
it 'takes user to her MR page when MR authored is clicked' do
@ -194,7 +198,8 @@ describe "Search", feature: true do
sleep 2
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

View File

@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user');
it('should not return the double quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: {
value: '"johnny appleseed',
},
lastToken: '"johnny appleseed',
});
expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user');
it('should not return the single quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
lastToken: {
value: '\'larry boy',
},
lastToken: '\'larry boy',
});
expect(dropdownUser.getSearchInput()).toBe('larry boy');

View File

@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
});
it('should filter without symbol', () => {
input.value = ':roo';
input.value = 'roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager');
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', () => {
const multipleWordItem = {
title: 'Community Contributions',
};
it('should filter with double quote', () => {
input.value = 'label:"';
input.value = '"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with double quote and symbol', () => {
input.value = 'label:~"';
input.value = '~"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
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);
expect(updatedItem.droplab_hidden).toBe(false);
});
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);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with single quote', () => {
input.value = 'label:\'';
input.value = '\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with single quote and symbol', () => {
input.value = 'label:~\'';
input.value = '~\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
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);
expect(updatedItem.droplab_hidden).toBe(false);
});
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);
expect(updatedItem.droplab_hidden).toBe(false);

View File

@ -1,4 +1,5 @@
require('~/extensions/array');
require('~/filtered_search/filtered_search_visual_tokens');
require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager');
@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager');
}
beforeEach(() => {
const input = document.createElement('input');
input.classList.add('filtered-search');
document.body.appendChild(input);
});
afterEach(() => {
document.querySelector('.filtered-search').outerHTML = '';
setFixtures(`
<ul class="tokens-container">
<li class="input-token">
<input class="filtered-search">
</li>
</ul>
`);
});
describe('input has no existing value', () => {
it('should add just tokenName', () => {
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', () => {
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');
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');
it('should be able to just add tokenName', () => {
setInputValue('a');
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', () => {
setInputValue('author:roo');
gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
expect(getInputValue()).toBe('author:@root ');
gl.FilteredSearchDropdownManager.addWordToInput('author');
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', () => {
setInputValue('label:~"test');
gl.FilteredSearchDropdownManager.addWordToInput('label');
setInputValue('"test ');
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('');
});
});
});

View File

@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys');
require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager');
require('~/filtered_search/filtered_search_manager');
const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
(() => {
describe('Filtered Search Manager', () => {
let input;
let manager;
let tokensContainer;
const placeholder = 'Search or filter results...';
function dispatchBackspaceEvent(element, eventType) {
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(() => {
setFixtures(`
<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, 'cleanup').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, 'updateDropdownOffset').and.callFake(() => {});
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();
});
describe('search', () => {
let manager;
const defaultParams = '?scope=all&utf8=✓&state=opened';
function getInput() {
return document.querySelector('.filtered-search');
}
beforeEach(() => {
setFixtures(`
<input type='text' class='filtered-search' />
`);
spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
manager = new gl.FilteredSearchManager();
});
afterEach(() => {
getInput().outerHTML = '';
});
it('should search with a single word', () => {
getInput().value = 'searchTerm';
it('should search with a single word', (done) => {
input.value = 'searchTerm';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
done();
});
manager.search();
});
it('should search with multiple words', () => {
getInput().value = 'awesome search terms';
it('should search with multiple words', (done) => {
input.value = 'awesome search terms';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done();
});
manager.search();
});
it('should search with special characters', () => {
getInput().value = '~!@#$%^&*()_+{}:<>,.?/';
it('should search with special characters', (done) => {
input.value = '~!@#$%^&*()_+{}:<>,.?/';
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`);
done();
});
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();
});
});
});
})();

View File

@ -0,0 +1,587 @@
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);
});
});
});

View File

@ -0,0 +1,52 @@
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;

View File

@ -3,16 +3,20 @@ module FilteredSearchHelpers
page.find('.filtered-search')
end
# Enables input to be set (similar to copy and paste)
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
filtered_search.send_keys(:enter)
end
end
# Enables input to be added character by character
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)
end
@ -34,4 +38,32 @@ module FilteredSearchHelpers
# This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
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