Commit 586b07a4 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'rjain-filter-bar' into 'master'

Add support for operator in filter bar

See merge request gitlab-org/gitlab!19011
parents d7394c88 57e556bd
...@@ -101,6 +101,11 @@ class DropDown { ...@@ -101,6 +101,11 @@ class DropDown {
render(data) { render(data) {
const children = data ? data.map(this.renderChildren.bind(this)) : []; const children = data ? data.map(this.renderChildren.bind(this)) : [];
if (this.list.querySelector('.filter-dropdown-loading')) {
return;
}
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list; const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
renderableList.innerHTML = children.join(''); renderableList.innerHTML = children.join('');
......
...@@ -2,6 +2,7 @@ import { __ } from '~/locale'; ...@@ -2,6 +2,7 @@ import { __ } from '~/locale';
export default IssuableTokenKeys => { export default IssuableTokenKeys => {
const wipToken = { const wipToken = {
formattedKey: __('WIP'),
key: 'wip', key: 'wip',
type: 'string', type: 'string',
param: '', param: '',
...@@ -17,6 +18,7 @@ export default IssuableTokenKeys => { ...@@ -17,6 +18,7 @@ export default IssuableTokenKeys => {
IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken); IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken);
const targetBranchToken = { const targetBranchToken = {
formattedKey: __('Target-Branch'),
key: 'target-branch', key: 'target-branch',
type: 'string', type: 'string',
param: '', param: '',
......
import { __ } from '~/locale';
import FilteredSearchTokenKeys from './filtered_search_token_keys'; import FilteredSearchTokenKeys from './filtered_search_token_keys';
const tokenKeys = [ const tokenKeys = [
{ {
formattedKey: __('Status'),
key: 'status', key: 'status',
type: 'string', type: 'string',
param: 'status', param: 'status',
...@@ -10,6 +12,7 @@ const tokenKeys = [ ...@@ -10,6 +12,7 @@ const tokenKeys = [
tag: 'status', tag: 'status',
}, },
{ {
formattedKey: __('Type'),
key: 'type', key: 'type',
type: 'string', type: 'string',
param: 'type', param: 'type',
...@@ -18,6 +21,7 @@ const tokenKeys = [ ...@@ -18,6 +21,7 @@ const tokenKeys = [
tag: 'type', tag: 'type',
}, },
{ {
formattedKey: __('Tag'),
key: 'tag', key: 'tag',
type: 'array', type: 'array',
param: 'name[]', param: 'name[]',
......
...@@ -4,6 +4,7 @@ import DropdownNonUser from './dropdown_non_user'; ...@@ -4,6 +4,7 @@ import DropdownNonUser from './dropdown_non_user';
import DropdownEmoji from './dropdown_emoji'; import DropdownEmoji from './dropdown_emoji';
import NullDropdown from './null_dropdown'; import NullDropdown from './null_dropdown';
import DropdownAjaxFilter from './dropdown_ajax_filter'; import DropdownAjaxFilter from './dropdown_ajax_filter';
import DropdownOperator from './dropdown_operator';
import DropdownUtils from './dropdown_utils'; import DropdownUtils from './dropdown_utils';
import { mergeUrlParams } from '../lib/utils/url_utility'; import { mergeUrlParams } from '../lib/utils/url_utility';
...@@ -40,6 +41,11 @@ export default class AvailableDropdownMappings { ...@@ -40,6 +41,11 @@ export default class AvailableDropdownMappings {
gl: DropdownHint, gl: DropdownHint,
element: this.container.querySelector('#js-dropdown-hint'), element: this.container.querySelector('#js-dropdown-hint'),
}, },
operator: {
reference: null,
gl: DropdownOperator,
element: this.container.querySelector('#js-dropdown-operator'),
},
}; };
supportedTokens.forEach(type => { supportedTokens.forEach(type => {
......
...@@ -29,6 +29,7 @@ export default { ...@@ -29,6 +29,7 @@ export default {
const resultantTokens = tokens.map(token => ({ const resultantTokens = tokens.map(token => ({
prefix: `${token.key}:`, prefix: `${token.key}:`,
operator: token.operator,
suffix: `${token.symbol}${token.value}`, suffix: `${token.symbol}${token.value}`,
})); }));
...@@ -75,6 +76,7 @@ export default { ...@@ -75,6 +76,7 @@ export default {
class="filtered-search-history-dropdown-token" class="filtered-search-history-dropdown-token"
> >
<span class="name">{{ token.prefix }}</span> <span class="name">{{ token.prefix }}</span>
<span class="name">{{ token.operator }}</span>
<span class="value">{{ token.suffix }}</span> <span class="value">{{ token.suffix }}</span>
</span> </span>
</span> </span>
......
/* eslint-disable import/prefer-default-export */
export const USER_TOKEN_TYPES = ['author', 'assignee']; export const USER_TOKEN_TYPES = ['author', 'assignee'];
export const DROPDOWN_TYPE = {
hint: 'hint',
operator: 'operator',
};
...@@ -45,7 +45,7 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown { ...@@ -45,7 +45,7 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown {
getSearchInput() { getSearchInput() {
const query = DropdownUtils.getSearchInput(this.input); const query = DropdownUtils.getSearchInput(this.input);
const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get()); const { lastToken } = FilteredSearchTokenizer.processTokens(query, this.tokenKeys.getKeys());
let value = lastToken || ''; let value = lastToken || '';
......
...@@ -3,6 +3,7 @@ import FilteredSearchDropdown from './filtered_search_dropdown'; ...@@ -3,6 +3,7 @@ import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils'; import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
import { __ } from '~/locale';
export default class DropdownHint extends FilteredSearchDropdown { export default class DropdownHint extends FilteredSearchDropdown {
constructor(options = {}) { constructor(options = {}) {
...@@ -30,8 +31,8 @@ export default class DropdownHint extends FilteredSearchDropdown { ...@@ -30,8 +31,8 @@ export default class DropdownHint extends FilteredSearchDropdown {
this.dismissDropdown(); this.dismissDropdown();
this.dispatchFormSubmitEvent(); this.dispatchFormSubmitEvent();
} else { } else {
const token = selected.querySelector('.js-filter-hint').innerText.trim(); const filterItemEl = selected.closest('.filter-dropdown-item');
const tag = selected.querySelector('.js-filter-tag').innerText.trim(); const { hint: token, tag } = filterItemEl.dataset;
if (tag.length) { if (tag.length) {
// Get previous input values in the input field and convert them into visual tokens // Get previous input values in the input field and convert them into visual tokens
...@@ -55,8 +56,13 @@ export default class DropdownHint extends FilteredSearchDropdown { ...@@ -55,8 +56,13 @@ export default class DropdownHint extends FilteredSearchDropdown {
const key = token.replace(':', ''); const key = token.replace(':', '');
const { uppercaseTokenName } = this.tokenKeys.searchByKey(key); const { uppercaseTokenName } = this.tokenKeys.searchByKey(key);
FilteredSearchDropdownManager.addWordToInput(key, '', false, {
uppercaseTokenName, FilteredSearchDropdownManager.addWordToInput({
tokenName: key,
clicked: false,
options: {
uppercaseTokenName,
},
}); });
} }
this.dismissDropdown(); this.dismissDropdown();
...@@ -66,15 +72,30 @@ export default class DropdownHint extends FilteredSearchDropdown { ...@@ -66,15 +72,30 @@ export default class DropdownHint extends FilteredSearchDropdown {
} }
renderContent() { renderContent() {
const dropdownData = this.tokenKeys.get().map(tokenKey => ({ const searchItem = [
icon: `${gon.sprite_icons}#${tokenKey.icon}`, {
hint: tokenKey.key, hint: 'search',
tag: `:${tokenKey.tag}`, tag: 'search',
type: tokenKey.type, formattedKey: __('Search for this text'),
})); icon: `${gon.sprite_icons}#search`,
},
];
const dropdownData = this.tokenKeys
.get()
.map(tokenKey => ({
icon: `${gon.sprite_icons}#${tokenKey.icon}`,
hint: tokenKey.key,
tag: `:${tokenKey.tag}`,
type: tokenKey.type,
formattedKey: tokenKey.formattedKey,
}))
.concat(searchItem);
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData); this.droplab.setData(this.hookId, dropdownData);
super.renderContent();
} }
init() { init() {
......
import Filter from '~/droplab/plugins/filter';
import { __ } from '~/locale';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class DropdownOperator extends FilteredSearchDropdown {
constructor(options = {}) {
const { input, tokenKeys } = options;
super(options);
this.config = {
Filter: {
filterFunction: DropdownUtils.filterWithSymbol.bind(null, '', input),
template: 'title',
},
};
this.tokenKeys = tokenKeys;
}
itemClicked(e) {
const { selected } = e.detail;
if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) {
const operator = selected.dataset.value;
FilteredSearchVisualTokens.removeLastTokenPartial();
FilteredSearchDropdownManager.addWordToInput({
tokenName: this.filter,
tokenOperator: operator,
clicked: false,
});
}
}
this.dismissDropdown();
this.dispatchInputEvent();
}
renderContent(forceShowList = false) {
this.filter = FilteredSearchVisualTokens.getLastTokenPartial();
const dropdownData = [
{
tag: 'equal',
type: 'string',
title: '=',
help: __('Is'),
},
{
tag: 'not-equal',
type: 'string',
title: '!=',
help: __('Is not'),
},
];
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData);
super.renderContent(forceShowList);
}
init() {
this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
}
}
...@@ -62,28 +62,42 @@ export default class DropdownUtils { ...@@ -62,28 +62,42 @@ export default class DropdownUtils {
const lastKey = lastToken.key || lastToken || ''; const lastKey = lastToken.key || lastToken || '';
const allowMultiple = item.type === 'array'; const allowMultiple = item.type === 'array';
const itemInExistingTokens = tokens.some(t => t.key === item.hint); const itemInExistingTokens = tokens.some(t => t.key === item.hint);
const isSearchItem = updatedItem.hint === 'search';
if (isSearchItem) {
updatedItem.droplab_hidden = true;
}
if (!allowMultiple && itemInExistingTokens) { if (!allowMultiple && itemInExistingTokens) {
updatedItem.droplab_hidden = true; updatedItem.droplab_hidden = true;
} else if (!lastKey || _.last(searchInput.split('')) === ' ') { } else if (!isSearchItem && (!lastKey || _.last(searchInput.split('')) === ' ')) {
updatedItem.droplab_hidden = false; updatedItem.droplab_hidden = false;
} else if (lastKey) { } else if (lastKey) {
const split = lastKey.split(':'); const split = lastKey.split(':');
const tokenName = _.last(split[0].split(' ')); const tokenName = _.last(split[0].split(' '));
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; const match = isSearchItem
? allowedKeys.some(key => key.startsWith(tokenName.toLowerCase()))
: updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
updatedItem.droplab_hidden = tokenName ? match : false; updatedItem.droplab_hidden = tokenName ? match : false;
} }
return updatedItem; return updatedItem;
} }
static setDataValueIfSelected(filter, selected) { static setDataValueIfSelected(filter, operator, selected) {
const dataValue = selected.getAttribute('data-value'); const dataValue = selected.getAttribute('data-value');
if (dataValue) { if (dataValue) {
FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true, { FilteredSearchDropdownManager.addWordToInput({
capitalizeTokenValue: selected.hasAttribute('data-capitalize'), tokenName: filter,
tokenOperator: operator,
tokenValue: dataValue,
clicked: true,
options: {
capitalizeTokenValue: selected.hasAttribute('data-capitalize'),
},
}); });
} }
...@@ -101,7 +115,11 @@ export default class DropdownUtils { ...@@ -101,7 +115,11 @@ export default class DropdownUtils {
// remove leading symbol and wrapping quotes // remove leading symbol and wrapping quotes
tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
} }
return { tokenName, tokenValue };
const operatorEl = visualToken && visualToken.querySelector('.operator');
const tokenOperator = operatorEl && operatorEl.textContent.trim();
return { tokenName, tokenOperator, tokenValue };
} }
// Determines the full search query (visual tokens + input) // Determines the full search query (visual tokens + input)
...@@ -119,10 +137,16 @@ export default class DropdownUtils { ...@@ -119,10 +137,16 @@ export default class DropdownUtils {
tokens.forEach(token => { tokens.forEach(token => {
if (token.classList.contains('js-visual-token')) { if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name'); const name = token.querySelector('.name');
const operatorContainer = token.querySelector('.operator');
const value = token.querySelector('.value'); const value = token.querySelector('.value');
const valueContainer = token.querySelector('.value-container'); const valueContainer = token.querySelector('.value-container');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = ''; let valueText = '';
let operator = '';
if (operatorContainer) {
operator = operatorContainer.textContent.trim();
}
if (valueContainer && valueContainer.dataset.originalValue) { if (valueContainer && valueContainer.dataset.originalValue) {
valueText = valueContainer.dataset.originalValue; valueText = valueContainer.dataset.originalValue;
...@@ -131,7 +155,7 @@ export default class DropdownUtils { ...@@ -131,7 +155,7 @@ export default class DropdownUtils {
} }
if (token.className.indexOf('filtered-search-token') !== -1) { if (token.className.indexOf('filtered-search-token') !== -1) {
values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`); values.push(`${name.innerText.toLowerCase()}:${operator}${symbol}${valueText}`);
} else { } else {
values.push(name.innerText); values.push(name.innerText);
} }
......
import DropdownUtils from './dropdown_utils'; import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
...@@ -31,13 +32,26 @@ export default class FilteredSearchDropdown { ...@@ -31,13 +32,26 @@ export default class FilteredSearchDropdown {
itemClicked(e, getValueFunction) { itemClicked(e, getValueFunction) {
const { selected } = e.detail; const { selected } = e.detail;
if (selected.tagName === 'LI' && selected.innerHTML) { if (selected.tagName === 'LI' && selected.innerHTML) {
const dataValueSet = DropdownUtils.setDataValueIfSelected(this.filter, selected); const {
lastVisualToken: visualToken,
} = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const { tokenOperator } = DropdownUtils.getVisualTokenValues(visualToken);
const dataValueSet = DropdownUtils.setDataValueIfSelected(
this.filter,
tokenOperator,
selected,
);
if (!dataValueSet) { if (!dataValueSet) {
const value = getValueFunction(selected); const value = getValueFunction(selected);
FilteredSearchDropdownManager.addWordToInput(this.filter, value, true); FilteredSearchDropdownManager.addWordToInput({
tokenName: this.filter,
tokenOperator,
tokenValue: value,
clicked: true,
});
} }
this.resetFilters(); this.resetFilters();
......
...@@ -5,6 +5,7 @@ import FilteredSearchContainer from './container'; ...@@ -5,6 +5,7 @@ import FilteredSearchContainer from './container';
import FilteredSearchTokenKeys from './filtered_search_token_keys'; import FilteredSearchTokenKeys from './filtered_search_token_keys';
import DropdownUtils from './dropdown_utils'; import DropdownUtils from './dropdown_utils';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
import { DROPDOWN_TYPE } from './constants';
export default class FilteredSearchDropdownManager { export default class FilteredSearchDropdownManager {
constructor({ constructor({
...@@ -67,10 +68,16 @@ export default class FilteredSearchDropdownManager { ...@@ -67,10 +68,16 @@ export default class FilteredSearchDropdownManager {
this.mapping = availableMappings.getAllowedMappings(supportedTokens); this.mapping = availableMappings.getAllowedMappings(supportedTokens);
} }
static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { static addWordToInput({
tokenName,
tokenOperator = '',
tokenValue = '',
clicked = false,
options = {},
}) {
const { uppercaseTokenName = false, capitalizeTokenValue = false } = options; const { uppercaseTokenName = false, capitalizeTokenValue = false } = options;
const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, { FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenOperator, tokenValue, {
uppercaseTokenName, uppercaseTokenName,
capitalizeTokenValue, capitalizeTokenValue,
}); });
...@@ -129,7 +136,10 @@ export default class FilteredSearchDropdownManager { ...@@ -129,7 +136,10 @@ export default class FilteredSearchDropdownManager {
mappingKey.reference.init(); mappingKey.reference.init();
} }
if (this.currentDropdown === 'hint') { if (
this.currentDropdown === DROPDOWN_TYPE.hint ||
this.currentDropdown === DROPDOWN_TYPE.operator
) {
// Force the dropdown to show if it was clicked from the hint dropdown // Force the dropdown to show if it was clicked from the hint dropdown
forceShowList = true; forceShowList = true;
} }
...@@ -148,13 +158,19 @@ export default class FilteredSearchDropdownManager { ...@@ -148,13 +158,19 @@ export default class FilteredSearchDropdownManager {
this.droplab = new DropLab(); this.droplab = new DropLab();
} }
if (dropdownName === DROPDOWN_TYPE.operator) {
this.load(dropdownName, firstLoad);
return;
}
const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const match = this.filteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = const shouldOpenFilterDropdown =
match && this.currentDropdown !== match.key && this.mapping[match.key]; match && this.currentDropdown !== match.key && this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; const shouldOpenHintDropdown = !match && this.currentDropdown !== DROPDOWN_TYPE.hint;
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
const key = match && match.key ? match.key : 'hint'; const key = match && match.key ? match.key : DROPDOWN_TYPE.hint;
this.load(key, firstLoad); this.load(key, firstLoad);
} }
} }
...@@ -169,19 +185,32 @@ export default class FilteredSearchDropdownManager { ...@@ -169,19 +185,32 @@ export default class FilteredSearchDropdownManager {
if (this.currentDropdown) { if (this.currentDropdown) {
this.updateCurrentDropdownOffset(); this.updateCurrentDropdownOffset();
} }
if (lastToken === searchToken && lastToken !== null) { if (lastToken === searchToken && lastToken !== null) {
// Token is not fully initialized yet because it has no value // Token is not fully initialized yet because it has no value
// Eg. token = 'label:' // Eg. token = 'label:'
const split = lastToken.split(':'); const split = lastToken.split(':');
const dropdownName = _.last(split[0].split(' ')); const dropdownName = _.last(split[0].split(' '));
this.loadDropdown(split.length > 1 ? dropdownName : ''); const possibleOperatorToken = _.last(split[1]);
const hasOperator = FilteredSearchVisualTokens.permissibleOperatorValues.includes(
possibleOperatorToken && possibleOperatorToken.trim(),
);
let dropdownToOpen = '';
if (split.length > 1) {
const lastOperatorToken = FilteredSearchVisualTokens.getLastTokenOperator();
dropdownToOpen = hasOperator && lastOperatorToken ? dropdownName : DROPDOWN_TYPE.operator;
}
this.loadDropdown(dropdownToOpen);
} else if (lastToken) { } else if (lastToken) {
const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator();
// Token has been initialized into an object because it has a value // Token has been initialized into an object because it has a value
this.loadDropdown(lastToken.key); this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator);
} else { } else {
this.loadDropdown('hint'); this.loadDropdown(DROPDOWN_TYPE.hint);
} }
} }
......
...@@ -65,17 +65,20 @@ export default class FilteredSearchTokenKeys { ...@@ -65,17 +65,20 @@ export default class FilteredSearchTokenKeys {
return this.conditions.find(condition => condition.url === url) || null; return this.conditions.find(condition => condition.url === url) || null;
} }
searchByConditionKeyValue(key, value) { searchByConditionKeyValue(key, operator, value) {
return ( return (
this.conditions.find( this.conditions.find(
condition => condition =>
condition.tokenKey === key && condition.value.toLowerCase() === value.toLowerCase(), condition.tokenKey === key &&
condition.operator === operator &&
condition.value.toLowerCase() === value.toLowerCase(),
) || null ) || null
); );
} }
addExtraTokensForIssues() { addExtraTokensForIssues() {
const confidentialToken = { const confidentialToken = {
formattedKey: __('Confidential'),
key: 'confidential', key: 'confidential',
type: 'string', type: 'string',
param: '', param: '',
......
...@@ -2,10 +2,11 @@ import './filtered_search_token_keys'; ...@@ -2,10 +2,11 @@ import './filtered_search_token_keys';
export default class FilteredSearchTokenizer { export default class FilteredSearchTokenizer {
static processTokens(input, allowedKeys) { static processTokens(input, allowedKeys) {
// Regex extracts `(token):(symbol)(value)` // Regex extracts `(token):(operator)(symbol)(value)`
// Values that start with a double quote must end in a double quote (same for single) // Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = new RegExp( const tokenRegex = new RegExp(
`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, `(${allowedKeys.join('|')}):(=|!=)?([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`,
'g', 'g',
); );
const tokens = []; const tokens = [];
...@@ -13,16 +14,22 @@ export default class FilteredSearchTokenizer { ...@@ -13,16 +14,22 @@ export default class FilteredSearchTokenizer {
let lastToken = null; let lastToken = null;
const searchToken = const searchToken =
input input
.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { .replace(tokenRegex, (match, key, operator, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3; let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol; let tokenSymbol = symbol;
let tokenIndex = ''; let tokenIndex = '';
let tokenOperator = operator;
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue; tokenSymbol = tokenValue;
tokenValue = ''; tokenValue = '';
} }
if (tokenValue === '!=' || tokenValue === '=') {
tokenOperator = tokenValue;
tokenValue = '';
}
tokenIndex = `${key}:${tokenValue}`; tokenIndex = `${key}:${tokenValue}`;
// Prevent adding duplicates // Prevent adding duplicates
...@@ -33,6 +40,7 @@ export default class FilteredSearchTokenizer { ...@@ -33,6 +40,7 @@ export default class FilteredSearchTokenizer {
key, key,
value: tokenValue || '', value: tokenValue || '',
symbol: tokenSymbol || '', symbol: tokenSymbol || '',
operator: tokenOperator || '',
}); });
} }
...@@ -43,13 +51,12 @@ export default class FilteredSearchTokenizer { ...@@ -43,13 +51,12 @@ export default class FilteredSearchTokenizer {
if (tokens.length > 0) { if (tokens.length > 0) {
const last = tokens[tokens.length - 1]; const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`; const lastString = `${last.key}:${last.operator}${last.symbol}${last.value}`;
lastToken = lastToken =
input.lastIndexOf(lastString) === input.length - lastString.length ? last : searchToken; input.lastIndexOf(lastString) === input.length - lastString.length ? last : searchToken;
} else { } else {
lastToken = searchToken; lastToken = searchToken;
} }
return { return {
tokens, tokens,
lastToken, lastToken,
......
import { flatten } from 'underscore';
import FilteredSearchTokenKeys from './filtered_search_token_keys'; import FilteredSearchTokenKeys from './filtered_search_token_keys';
import { __ } from '~/locale'; import { __ } from '~/locale';
export const tokenKeys = [ export const tokenKeys = [
{ {
formattedKey: __('Author'),
key: 'author', key: 'author',
type: 'string', type: 'string',
param: 'username', param: 'username',
...@@ -11,6 +13,7 @@ export const tokenKeys = [ ...@@ -11,6 +13,7 @@ export const tokenKeys = [
tag: '@author', tag: '@author',
}, },
{ {
formattedKey: __('Assignee'),
key: 'assignee', key: 'assignee',
type: 'string', type: 'string',
param: 'username', param: 'username',
...@@ -19,6 +22,7 @@ export const tokenKeys = [ ...@@ -19,6 +22,7 @@ export const tokenKeys = [
tag: '@assignee', tag: '@assignee',
}, },
{ {
formattedKey: __('Milestone'),
key: 'milestone', key: 'milestone',
type: 'string', type: 'string',
param: 'title', param: 'title',
...@@ -27,6 +31,7 @@ export const tokenKeys = [ ...@@ -27,6 +31,7 @@ export const tokenKeys = [
tag: '%milestone', tag: '%milestone',
}, },
{ {
formattedKey: __('Release'),
key: 'release', key: 'release',
type: 'string', type: 'string',
param: 'tag', param: 'tag',
...@@ -35,6 +40,7 @@ export const tokenKeys = [ ...@@ -35,6 +40,7 @@ export const tokenKeys = [
tag: __('tag name'), tag: __('tag name'),
}, },
{ {
formattedKey: __('Label'),
key: 'label', key: 'label',
type: 'array', type: 'array',
param: 'name[]', param: 'name[]',
...@@ -47,6 +53,7 @@ export const tokenKeys = [ ...@@ -47,6 +53,7 @@ export const tokenKeys = [
if (gon.current_user_id) { if (gon.current_user_id) {
// Appending tokenkeys only logged-in // Appending tokenkeys only logged-in
tokenKeys.push({ tokenKeys.push({
formattedKey: __('My-Reaction'),
key: 'my-reaction', key: 'my-reaction',
type: 'string', type: 'string',
param: 'emoji', param: 'emoji',
...@@ -58,6 +65,7 @@ if (gon.current_user_id) { ...@@ -58,6 +65,7 @@ if (gon.current_user_id) {
export const alternativeTokenKeys = [ export const alternativeTokenKeys = [
{ {
formattedKey: __('Label'),
key: 'label', key: 'label',
type: 'string', type: 'string',
param: 'name', param: 'name',
...@@ -65,68 +73,88 @@ export const alternativeTokenKeys = [ ...@@ -65,68 +73,88 @@ export const alternativeTokenKeys = [
}, },
]; ];
export const conditions = [ export const conditions = flatten(
{ [
url: 'assignee_id=None', {
tokenKey: 'assignee', url: 'assignee_id=None',
value: __('None'), tokenKey: 'assignee',
}, value: __('None'),
{ },
url: 'assignee_id=Any', {
tokenKey: 'assignee', url: 'assignee_id=Any',
value: __('Any'), tokenKey: 'assignee',
}, value: __('Any'),
{ },
url: 'milestone_title=None', {
tokenKey: 'milestone', url: 'milestone_title=None',
value: __('None'), tokenKey: 'milestone',
}, value: __('None'),
{ },
url: 'milestone_title=Any', {
tokenKey: 'milestone', url: 'milestone_title=Any',
value: __('Any'), tokenKey: 'milestone',
}, value: __('Any'),
{ },
url: 'milestone_title=%23upcoming', {
tokenKey: 'milestone', url: 'milestone_title=%23upcoming',
value: __('Upcoming'), tokenKey: 'milestone',
}, value: __('Upcoming'),
{ },
url: 'milestone_title=%23started', {
tokenKey: 'milestone', url: 'milestone_title=%23started',
value: __('Started'), tokenKey: 'milestone',
}, value: __('Started'),
{ },
url: 'release_tag=None', {
tokenKey: 'release', url: 'release_tag=None',
value: __('None'), tokenKey: 'release',
}, value: __('None'),
{ },
url: 'release_tag=Any', {
tokenKey: 'release', url: 'release_tag=Any',
value: __('Any'), tokenKey: 'release',
}, value: __('Any'),
{ },
url: 'label_name[]=None', {
tokenKey: 'label', url: 'label_name[]=None',
value: __('None'), tokenKey: 'label',
}, value: __('None'),
{ },
url: 'label_name[]=Any', {
tokenKey: 'label', url: 'label_name[]=Any',
value: __('Any'), tokenKey: 'label',
}, value: __('Any'),
{ },
url: 'my_reaction_emoji=None', {
tokenKey: 'my-reaction', url: 'my_reaction_emoji=None',
value: __('None'), tokenKey: 'my-reaction',
}, value: __('None'),
{ },
url: 'my_reaction_emoji=Any', {
tokenKey: 'my-reaction', url: 'my_reaction_emoji=Any',
value: __('Any'), tokenKey: 'my-reaction',
}, value: __('Any'),
]; },
].map(condition => {
const [keyPart, valuePart] = condition.url.split('=');
const hasBrackets = keyPart.includes('[]');
const notEqualUrl = `not[${hasBrackets ? keyPart.slice(0, -2) : keyPart}]${
hasBrackets ? '[]' : ''
}=${valuePart}`;
return [
{
...condition,
operator: '=',
},
{
...condition,
operator: '!=',
url: notEqualUrl,
},
];
}),
);
const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys( const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys(
tokenKeys, tokenKeys,
......
...@@ -9,9 +9,10 @@ import UsersCache from '~/lib/utils/users_cache'; ...@@ -9,9 +9,10 @@ import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default class VisualTokenValue { export default class VisualTokenValue {
constructor(tokenValue, tokenType) { constructor(tokenValue, tokenType, tokenOperator) {
this.tokenValue = tokenValue; this.tokenValue = tokenValue;
this.tokenType = tokenType; this.tokenType = tokenType;
this.tokenOperator = tokenOperator;
} }
render(tokenValueContainer, tokenValueElement) { render(tokenValueContainer, tokenValueElement) {
......
...@@ -2,3 +2,4 @@ export const UP_KEY_CODE = 38; ...@@ -2,3 +2,4 @@ export const UP_KEY_CODE = 38;
export const DOWN_KEY_CODE = 40; export const DOWN_KEY_CODE = 40;
export const ENTER_KEY_CODE = 13; export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27; export const ESC_KEY_CODE = 27;
export const BACKSPACE_KEY_CODE = 8;
...@@ -88,6 +88,7 @@ ...@@ -88,6 +88,7 @@
} }
.name, .name,
.operator,
.value { .value {
display: inline-block; display: inline-block;
padding: 2px 7px; padding: 2px 7px;
...@@ -101,6 +102,12 @@ ...@@ -101,6 +102,12 @@
text-transform: capitalize; text-transform: capitalize;
} }
.operator {
background-color: $white-normal;
color: $filter-value-text-color;
margin-right: 1px;
}
.value-container { .value-container {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -147,6 +154,10 @@ ...@@ -147,6 +154,10 @@
background-color: $filter-name-selected-color; background-color: $filter-name-selected-color;
} }
.operator {
box-shadow: inset 0 0 0 100px $filtered-search-term-shadow-color;
}
.value-container { .value-container {
box-shadow: inset 0 0 0 100px $filtered-search-term-shadow-color; box-shadow: inset 0 0 0 100px $filtered-search-term-shadow-color;
} }
...@@ -260,6 +271,11 @@ ...@@ -260,6 +271,11 @@
max-width: none; max-width: none;
min-width: 100%; min-width: 100%;
} }
.btn-helptext {
margin-left: auto;
color: var(--gray);
}
} }
.filtered-search-history-dropdown-wrapper { .filtered-search-history-dropdown-wrapper {
......
...@@ -90,7 +90,7 @@ module Boards ...@@ -90,7 +90,7 @@ module Boards
end end
def filter_params def filter_params
params.merge(board_id: params[:board_id], id: params[:list_id]) params.permit(*Boards::Issues::ListService.valid_params).merge(board_id: params[:board_id], id: params[:list_id])
.reject { |_, value| value.nil? } .reject { |_, value| value.nil? }
end end
......
...@@ -87,7 +87,7 @@ class IssuableFinder ...@@ -87,7 +87,7 @@ class IssuableFinder
end end
def valid_params def valid_params
@valid_params ||= scalar_params + [array_params] + [{ not: [] }] @valid_params ||= scalar_params + [array_params.merge(not: {})]
end end
end end
......
...@@ -5,6 +5,10 @@ module Boards ...@@ -5,6 +5,10 @@ module Boards
class ListService < Boards::BaseService class ListService < Boards::BaseService
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def self.valid_params
IssuesFinder.valid_params
end
def execute def execute
fetch_issues.order_by_position_and_priority fetch_issues.order_by_position_and_priority
end end
......
...@@ -57,24 +57,22 @@ ...@@ -57,24 +57,22 @@
%li.input-token %li.input-token
%input.form-control.filtered-search{ search_filter_input_options('runners') } %input.form-control.filtered-search{ search_filter_input_options('runners') }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
= button_tag class: %w[btn btn-link] do
= sprite_icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
= button_tag class: %w[btn btn-link] do = button_tag class: %w[btn btn-link] do
-# Encapsulate static class name `{{icon}}` inside #{} to bypass -# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue -# haml lint's ClassAttributeWithStaticValue
%svg %svg
%use{ 'xlink:href': "#{'{{icon}}'}" } %use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint %span.js-filter-hint
{{hint}} {{formattedKey}}
%span.js-filter-tag.dropdown-light-content #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu
{{tag}} %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } }
%li.filter-dropdown-item{ data: { value: "{{ title }}" } }
%button.btn.btn-link{ type: 'button' }
{{ title }}
%span.btn-helptext
{{ help }}
#js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
- Ci::Runner::AVAILABLE_STATUSES.each do |status| - Ci::Runner::AVAILABLE_STATUSES.each do |status|
......
...@@ -30,23 +30,22 @@ ...@@ -30,23 +30,22 @@
%li.input-token %li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) } %input.form-control.filtered-search{ search_filter_input_options(type) }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
%button.btn.btn-link{ type: 'button' }
= sprite_icon('search')
%span
= _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
%button.btn.btn-link{ type: 'button' } %button.btn.btn-link{ type: 'button' }
-# Encapsulate static class name `{{icon}}` inside #{} to bypass -# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue -# haml lint's ClassAttributeWithStaticValue
%svg %svg
%use{ 'xlink:href': "#{'{{icon}}'}" } %use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint %span.js-filter-hint
{{hint}} {{formattedKey}}
%span.js-filter-tag.dropdown-light-content #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu
{{tag}} %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } }
%li.filter-dropdown-item{ data: { value: "{{ title }}" } }
%button.btn.btn-link{ type: 'button' }
{{ title }}
%span.btn-helptext
{{ help }}
#js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu
- if current_user - if current_user
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
......
---
title: Add support for operator in filter bar
merge_request: 19011
author:
type: added
import { __ } from '~/locale';
import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
const tokenKeys = [ const tokenKeys = [
{ {
formattedKey: __('Author'),
key: 'author', key: 'author',
type: 'string', type: 'string',
param: 'username', param: 'username',
...@@ -10,6 +12,7 @@ const tokenKeys = [ ...@@ -10,6 +12,7 @@ const tokenKeys = [
tag: '@author', tag: '@author',
}, },
{ {
formattedKey: __('Milestone'),
key: 'milestone', key: 'milestone',
type: 'string', type: 'string',
param: 'title', param: 'title',
...@@ -18,6 +21,7 @@ const tokenKeys = [ ...@@ -18,6 +21,7 @@ const tokenKeys = [
tag: '%milestone', tag: '%milestone',
}, },
{ {
formattedKey: __('Label'),
key: 'label', key: 'label',
type: 'array', type: 'array',
param: 'name[]', param: 'name[]',
......
...@@ -9,15 +9,30 @@ export default IssuableTokenKeys => { ...@@ -9,15 +9,30 @@ export default IssuableTokenKeys => {
url: 'approver_usernames[]=None', url: 'approver_usernames[]=None',
tokenKey: 'approver', tokenKey: 'approver',
value: __('None'), value: __('None'),
operator: '=',
},
{
url: 'not[approver_usernames][]=None',
tokenKey: 'approver',
value: __('None'),
operator: '!=',
}, },
{ {
url: 'approver_usernames[]=Any', url: 'approver_usernames[]=Any',
tokenKey: 'approver', tokenKey: 'approver',
value: __('Any'), value: __('Any'),
operator: '=',
},
{
url: 'not[approver_usernames][]=Any',
tokenKey: 'approver',
value: __('Any'),
operator: '!=',
}, },
]; ];
const approversToken = { const approversToken = {
formattedKey: __('Approver'),
key: 'approver', key: 'approver',
type: 'array', type: 'array',
param: 'usernames[]', param: 'usernames[]',
......
import { __ } from '~/locale';
import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
const tokenKeys = [ const tokenKeys = [
{ {
formattedKey: __('Author'),
key: 'author', key: 'author',
type: 'string', type: 'string',
param: 'username', param: 'username',
...@@ -10,6 +12,7 @@ const tokenKeys = [ ...@@ -10,6 +12,7 @@ const tokenKeys = [
tag: '@author', tag: '@author',
}, },
{ {
formattedKey: __('Label'),
key: 'label', key: 'label',
type: 'array', type: 'array',
param: 'name[]', param: 'name[]',
...@@ -21,6 +24,7 @@ const tokenKeys = [ ...@@ -21,6 +24,7 @@ const tokenKeys = [
const alternativeTokenKeys = [ const alternativeTokenKeys = [
{ {
formattedKey: __('Label'),
key: 'label', key: 'label',
type: 'string', type: 'string',
param: 'name', param: 'name',
...@@ -33,6 +37,13 @@ const conditions = [ ...@@ -33,6 +37,13 @@ const conditions = [
url: 'label_name[]=No+Label', url: 'label_name[]=No+Label',
tokenKey: 'label', tokenKey: 'label',
value: 'none', value: 'none',
operator: '=',
},
{
url: 'not[label_name][]=No+Label',
tokenKey: 'label',
value: 'none',
operator: '!=',
}, },
]; ];
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
import { __ } from '~/locale'; import { __ } from '~/locale';
const weightTokenKey = { const weightTokenKey = {
formattedKey: __('Weight'),
key: 'weight', key: 'weight',
type: 'string', type: 'string',
param: '', param: '',
...@@ -18,11 +19,25 @@ const weightTokenKey = { ...@@ -18,11 +19,25 @@ const weightTokenKey = {
const weightConditions = [ const weightConditions = [
{ {
url: 'weight=None', url: 'weight=None',
operator: '=',
tokenKey: 'weight', tokenKey: 'weight',
value: __('None'), value: __('None'),
}, },
{ {
url: 'weight=Any', url: 'weight=Any',
operator: '=',
tokenKey: 'weight',
value: __('Any'),
},
{
url: 'not[weight]=None',
operator: '!=',
tokenKey: 'weight',
value: __('None'),
},
{
url: 'not[weight]=Any',
operator: '!=',
tokenKey: 'weight', tokenKey: 'weight',
value: __('Any'), value: __('Any'),
}, },
......
...@@ -42,23 +42,22 @@ ...@@ -42,23 +42,22 @@
%li.input-token %li.input-token
%input.form-control.filtered-search{ epic_endpoint_query_params(search_filter_input_options(type)) } %input.form-control.filtered-search{ epic_endpoint_query_params(search_filter_input_options(type)) }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
%button.btn.btn-link{ type: 'button' }
= sprite_icon('search')
%span
Press Enter or click to search
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item %li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
%button.btn.btn-link{ type: 'button' } %button.btn.btn-link{ type: 'button' }
-# Encapsulate static class name `{{icon}}` inside #{} to bypass -# Encapsulate static class name `{{icon}}` inside #{} to bypass
-# haml lint's ClassAttributeWithStaticValue -# haml lint's ClassAttributeWithStaticValue
%svg %svg
%use{ 'xlink:href': "#{'{{icon}}'}" } %use{ 'xlink:href': "#{'{{icon}}'}" }
%span.js-filter-hint %span.js-filter-hint
{{hint}} {{formattedKey}}
%span.js-filter-tag.dropdown-light-content #js-dropdown-operator.filtered-search-input-dropdown-menu.dropdown-menu
{{tag}} %ul.filter-dropdown{ data: { dropdown: true, dynamic: true } }
%li.filter-dropdown-item{ data: { value: "{{ title }}" } }
%button.btn.btn-link{ type: 'button' }
{{ title }}
%span.btn-helptext
{{ help }}
#js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu
- if current_user - if current_user
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
......
...@@ -25,6 +25,6 @@ describe 'Issue Boards add issue modal', :js do ...@@ -25,6 +25,6 @@ describe 'Issue Boards add issue modal', :js do
wait_for_requests wait_for_requests
find('.add-issues-modal .filtered-search').click find('.add-issues-modal .filtered-search').click
expect(page.find('.filter-dropdown')).to have_content 'weight' expect(page.find('.filter-dropdown')).to have_content 'Weight'
end end
end end
...@@ -132,8 +132,8 @@ describe 'Scoped issue boards', :js do ...@@ -132,8 +132,8 @@ describe 'Scoped issue boards', :js do
filtered_search.click filtered_search.click
page.within('#js-dropdown-hint') do page.within('#js-dropdown-hint') do
expect(page).to have_content('label') expect(page).to have_content('Label')
expect(page).not_to have_content('assignee') expect(page).not_to have_content('Assignee')
end end
end end
...@@ -164,8 +164,8 @@ describe 'Scoped issue boards', :js do ...@@ -164,8 +164,8 @@ describe 'Scoped issue boards', :js do
filtered_search.click filtered_search.click
page.within('#js-dropdown-hint') do page.within('#js-dropdown-hint') do
expect(page).to have_content('label') expect(page).to have_content('Label')
expect(page).not_to have_content('weight') expect(page).not_to have_content('Weight')
end end
end end
...@@ -244,8 +244,8 @@ describe 'Scoped issue boards', :js do ...@@ -244,8 +244,8 @@ describe 'Scoped issue boards', :js do
filtered_search.click filtered_search.click
page.within('#js-dropdown-hint') do page.within('#js-dropdown-hint') do
expect(page).to have_content('label') expect(page).to have_content('Label')
expect(page).not_to have_content('milestone') expect(page).not_to have_content('Milestone')
end end
end end
end end
...@@ -293,7 +293,7 @@ describe 'Scoped issue boards', :js do ...@@ -293,7 +293,7 @@ describe 'Scoped issue boards', :js do
update_board_label(label_title) update_board_label(label_title)
input_filtered_search("label:~#{label_2_title}") input_filtered_search("label=~#{label_2_title}")
expect(page).to have_selector('.board-card', count: 0) expect(page).to have_selector('.board-card', count: 0)
end end
...@@ -338,8 +338,8 @@ describe 'Scoped issue boards', :js do ...@@ -338,8 +338,8 @@ describe 'Scoped issue boards', :js do
filtered_search.click filtered_search.click
page.within('#js-dropdown-hint') do page.within('#js-dropdown-hint') do
expect(page).to have_content('label') expect(page).to have_content('Label')
expect(page).not_to have_content('assignee') expect(page).not_to have_content('Assignee')
end end
end end
end end
...@@ -365,8 +365,8 @@ describe 'Scoped issue boards', :js do ...@@ -365,8 +365,8 @@ describe 'Scoped issue boards', :js do
filtered_search.click filtered_search.click
page.within('#js-dropdown-hint') do page.within('#js-dropdown-hint') do
expect(page).to have_content('label') expect(page).to have_content('Label')
expect(page).not_to have_content('weight') expect(page).not_to have_content('Weight')
end end
end end
end end
......
...@@ -24,7 +24,7 @@ describe 'epics list', :js do ...@@ -24,7 +24,7 @@ describe 'epics list', :js do
context 'editing author token' do context 'editing author token' do
before do before do
input_filtered_search('author:@root', submit: false) input_filtered_search('author=@root', submit: false)
first('.tokens-container .filtered-search-token').click first('.tokens-container .filtered-search-token').click
end end
...@@ -52,7 +52,7 @@ describe 'epics list', :js do ...@@ -52,7 +52,7 @@ describe 'epics list', :js do
context 'editing label token' do context 'editing label token' do
before do before do
input_filtered_search("label:~#{label.title}", submit: false) input_filtered_search("label=~#{label.title}", submit: false)
first('.tokens-container .filtered-search-token').click first('.tokens-container .filtered-search-token').click
end end
......
...@@ -21,7 +21,7 @@ describe 'Dropdown weight', :js do ...@@ -21,7 +21,7 @@ describe 'Dropdown weight', :js do
describe 'behavior' do describe 'behavior' do
it 'loads all the weights when opened' do it 'loads all the weights when opened' do
input_filtered_search('weight:', submit: false, extra_space: false) input_filtered_search('weight=', submit: false, extra_space: false)
expect_filtered_search_dropdown_results(filter_dropdown, 21) expect_filtered_search_dropdown_results(filter_dropdown, 21)
end end
......
...@@ -40,7 +40,7 @@ describe 'Filter issues weight', :js do ...@@ -40,7 +40,7 @@ describe 'Filter issues weight', :js do
describe 'only weight' do describe 'only weight' do
it 'filter issues by searched weight' do it 'filter issues by searched weight' do
input_filtered_search('weight:1') input_filtered_search('weight=1')
expect_issues_list_count(1) expect_issues_list_count(1)
end end
...@@ -48,7 +48,7 @@ describe 'Filter issues weight', :js do ...@@ -48,7 +48,7 @@ describe 'Filter issues weight', :js do
describe 'weight with other filters' do describe 'weight with other filters' do
it 'filters issues by searched weight and text' do it 'filters issues by searched weight and text' do
search = "weight:2 bug" search = "weight=2 bug"
input_filtered_search(search) input_filtered_search(search)
expect_issues_list_count(1) expect_issues_list_count(1)
...@@ -56,7 +56,7 @@ describe 'Filter issues weight', :js do ...@@ -56,7 +56,7 @@ describe 'Filter issues weight', :js do
end end
it 'filters issues by searched weight, author and text' do it 'filters issues by searched weight, author and text' do
search = "weight:2 author:@root bug" search = "weight=2 author=@root bug"
input_filtered_search(search) input_filtered_search(search)
expect_issues_list_count(1) expect_issues_list_count(1)
...@@ -64,7 +64,7 @@ describe 'Filter issues weight', :js do ...@@ -64,7 +64,7 @@ describe 'Filter issues weight', :js do
end end
it 'filters issues by searched weight, author, assignee and text' do it 'filters issues by searched weight, author, assignee and text' do
search = "weight:2 author:@root assignee:@root bug" search = "weight=2 author=@root assignee=@root bug"
input_filtered_search(search) input_filtered_search(search)
expect_issues_list_count(1) expect_issues_list_count(1)
...@@ -72,7 +72,7 @@ describe 'Filter issues weight', :js do ...@@ -72,7 +72,7 @@ describe 'Filter issues weight', :js do
end end
it 'filters issues by searched weight, author, assignee, label and text' do it 'filters issues by searched weight, author, assignee, label and text' do
search = "weight:2 author:@root assignee:@root label:~urgent bug" search = "weight=2 author=@root assignee=@root label=~urgent bug"
input_filtered_search(search) input_filtered_search(search)
expect_issues_list_count(1) expect_issues_list_count(1)
...@@ -80,7 +80,7 @@ describe 'Filter issues weight', :js do ...@@ -80,7 +80,7 @@ describe 'Filter issues weight', :js do
end end
it 'filters issues by searched weight, milestone and text' do it 'filters issues by searched weight, milestone and text' do
search = "weight:2 milestone:%version1 bug" search = "weight=2 milestone=%version1 bug"
input_filtered_search(search) input_filtered_search(search)
expect_issues_list_count(1) expect_issues_list_count(1)
......
...@@ -32,7 +32,7 @@ describe 'Merge Requests > User filters by approvers', :js do ...@@ -32,7 +32,7 @@ describe 'Merge Requests > User filters by approvers', :js do
context 'filtering by approver:none' do context 'filtering by approver:none' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('approver:none') input_filtered_search('approver=none')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
...@@ -45,7 +45,7 @@ describe 'Merge Requests > User filters by approvers', :js do ...@@ -45,7 +45,7 @@ describe 'Merge Requests > User filters by approvers', :js do
context 'filtering by approver:any' do context 'filtering by approver:any' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('approver:any') input_filtered_search('approver=any')
expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3) expect(page).to have_issuable_counts(open: 3, closed: 0, all: 3)
...@@ -58,7 +58,7 @@ describe 'Merge Requests > User filters by approvers', :js do ...@@ -58,7 +58,7 @@ describe 'Merge Requests > User filters by approvers', :js do
context 'filtering by approver:@username' do context 'filtering by approver:@username' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search("approver:@#{first_user.username}") input_filtered_search("approver=@#{first_user.username}")
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
...@@ -71,7 +71,7 @@ describe 'Merge Requests > User filters by approvers', :js do ...@@ -71,7 +71,7 @@ describe 'Merge Requests > User filters by approvers', :js do
context 'filtering by multiple approvers' do context 'filtering by multiple approvers' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search("approver:@#{first_user.username} approver:@#{user.username}") input_filtered_search("approver=@#{first_user.username} approver=@#{user.username}")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
...@@ -84,7 +84,7 @@ describe 'Merge Requests > User filters by approvers', :js do ...@@ -84,7 +84,7 @@ describe 'Merge Requests > User filters by approvers', :js do
context 'filtering by an approver from a group' do context 'filtering by an approver from a group' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search("approver:@#{group_user.username}") input_filtered_search("approver=@#{group_user.username}")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
......
...@@ -2,6 +2,7 @@ import IssuableFilteredSearchTokenKeys from 'ee/filtered_search/issuable_filtere ...@@ -2,6 +2,7 @@ import IssuableFilteredSearchTokenKeys from 'ee/filtered_search/issuable_filtere
describe('Issues Filtered Search Token Keys (EE)', () => { describe('Issues Filtered Search Token Keys (EE)', () => {
const weightTokenKey = { const weightTokenKey = {
formattedKey: 'Weight',
key: 'weight', key: 'weight',
type: 'string', type: 'string',
param: '', param: '',
...@@ -41,7 +42,7 @@ describe('Issues Filtered Search Token Keys (EE)', () => { ...@@ -41,7 +42,7 @@ describe('Issues Filtered Search Token Keys (EE)', () => {
it('should return weightConditions as part of conditions', () => { it('should return weightConditions as part of conditions', () => {
const weightConditions = conditions.filter(c => c.tokenKey === 'weight'); const weightConditions = conditions.filter(c => c.tokenKey === 'weight');
expect(weightConditions.length).toBe(2); expect(weightConditions.length).toBe(4);
}); });
}); });
...@@ -91,6 +92,7 @@ describe('Issues Filtered Search Token Keys (EE)', () => { ...@@ -91,6 +92,7 @@ describe('Issues Filtered Search Token Keys (EE)', () => {
const weightConditions = conditions.filter(c => c.tokenKey === 'weight'); const weightConditions = conditions.filter(c => c.tokenKey === 'weight');
const result = IssuableFilteredSearchTokenKeys.searchByConditionKeyValue( const result = IssuableFilteredSearchTokenKeys.searchByConditionKeyValue(
weightConditions[0].tokenKey, weightConditions[0].tokenKey,
weightConditions[0].operator,
weightConditions[0].value, weightConditions[0].value,
); );
......
...@@ -2032,6 +2032,9 @@ msgstr "" ...@@ -2032,6 +2032,9 @@ msgstr ""
msgid "Approved the current merge request." msgid "Approved the current merge request."
msgstr "" msgstr ""
msgid "Approver"
msgstr ""
msgid "Apr" msgid "Apr"
msgstr "" msgstr ""
...@@ -9948,6 +9951,12 @@ msgstr "" ...@@ -9948,6 +9951,12 @@ msgstr ""
msgid "Invocations" msgid "Invocations"
msgstr "" msgstr ""
msgid "Is"
msgstr ""
msgid "Is not"
msgstr ""
msgid "Is using license seat:" msgid "Is using license seat:"
msgstr "" msgstr ""
...@@ -11678,6 +11687,9 @@ msgstr "" ...@@ -11678,6 +11687,9 @@ msgstr ""
msgid "Multiple uploaders found: %{uploader_types}" msgid "Multiple uploaders found: %{uploader_types}"
msgstr "" msgstr ""
msgid "My-Reaction"
msgstr ""
msgid "Name" msgid "Name"
msgstr "" msgstr ""
...@@ -13305,9 +13317,6 @@ msgstr "" ...@@ -13305,9 +13317,6 @@ msgstr ""
msgid "Press %{key}-C to copy" msgid "Press %{key}-C to copy"
msgstr "" msgstr ""
msgid "Press Enter or click to search"
msgstr ""
msgid "Prevent adding new members to project membership within this group" msgid "Prevent adding new members to project membership within this group"
msgstr "" msgstr ""
...@@ -15712,6 +15721,9 @@ msgstr "" ...@@ -15712,6 +15721,9 @@ msgstr ""
msgid "Search for projects, issues, etc." msgid "Search for projects, issues, etc."
msgstr "" msgstr ""
msgid "Search for this text"
msgstr ""
msgid "Search forks" msgid "Search forks"
msgstr "" msgstr ""
...@@ -17722,6 +17734,9 @@ msgstr "" ...@@ -17722,6 +17734,9 @@ msgstr ""
msgid "Target branch" msgid "Target branch"
msgstr "" msgstr ""
msgid "Target-Branch"
msgstr ""
msgid "Team" msgid "Team"
msgstr "" msgstr ""
...@@ -20291,6 +20306,9 @@ msgstr "" ...@@ -20291,6 +20306,9 @@ msgstr ""
msgid "Vulnerability|Severity" msgid "Vulnerability|Severity"
msgstr "" msgstr ""
msgid "WIP"
msgstr ""
msgid "Wait for the file to load to copy its contents" msgid "Wait for the file to load to copy its contents"
msgstr "" msgstr ""
......
...@@ -57,7 +57,7 @@ describe "Admin Runners" do ...@@ -57,7 +57,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-active' expect(page).to have_content 'runner-active'
expect(page).to have_content 'runner-paused' expect(page).to have_content 'runner-paused'
input_filtered_search_keys('status:active') input_filtered_search_keys('status=active')
expect(page).to have_content 'runner-active' expect(page).to have_content 'runner-active'
expect(page).not_to have_content 'runner-paused' expect(page).not_to have_content 'runner-paused'
end end
...@@ -68,7 +68,7 @@ describe "Admin Runners" do ...@@ -68,7 +68,7 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('status:offline') input_filtered_search_keys('status=offline')
expect(page).not_to have_content 'runner-active' expect(page).not_to have_content 'runner-active'
expect(page).not_to have_content 'runner-paused' expect(page).not_to have_content 'runner-paused'
...@@ -83,12 +83,12 @@ describe "Admin Runners" do ...@@ -83,12 +83,12 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('status:active') input_filtered_search_keys('status=active')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).to have_content 'runner-b-1' expect(page).to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
input_filtered_search_keys('status:active runner-a') input_filtered_search_keys('status=active runner-a')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).not_to have_content 'runner-b-1' expect(page).not_to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
...@@ -105,7 +105,7 @@ describe "Admin Runners" do ...@@ -105,7 +105,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-project'
expect(page).to have_content 'runner-group' expect(page).to have_content 'runner-group'
input_filtered_search_keys('type:project_type') input_filtered_search_keys('type=project_type')
expect(page).to have_content 'runner-project' expect(page).to have_content 'runner-project'
expect(page).not_to have_content 'runner-group' expect(page).not_to have_content 'runner-group'
end end
...@@ -116,7 +116,7 @@ describe "Admin Runners" do ...@@ -116,7 +116,7 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('type:instance_type') input_filtered_search_keys('type=instance_type')
expect(page).not_to have_content 'runner-project' expect(page).not_to have_content 'runner-project'
expect(page).not_to have_content 'runner-group' expect(page).not_to have_content 'runner-group'
...@@ -131,12 +131,12 @@ describe "Admin Runners" do ...@@ -131,12 +131,12 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('type:project_type') input_filtered_search_keys('type=project_type')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).to have_content 'runner-b-1' expect(page).to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
input_filtered_search_keys('type:project_type runner-a') input_filtered_search_keys('type=project_type runner-a')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).not_to have_content 'runner-b-1' expect(page).not_to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
...@@ -153,7 +153,7 @@ describe "Admin Runners" do ...@@ -153,7 +153,7 @@ describe "Admin Runners" do
expect(page).to have_content 'runner-blue' expect(page).to have_content 'runner-blue'
expect(page).to have_content 'runner-red' expect(page).to have_content 'runner-red'
input_filtered_search_keys('tag:blue') input_filtered_search_keys('tag=blue')
expect(page).to have_content 'runner-blue' expect(page).to have_content 'runner-blue'
expect(page).not_to have_content 'runner-red' expect(page).not_to have_content 'runner-red'
...@@ -165,7 +165,7 @@ describe "Admin Runners" do ...@@ -165,7 +165,7 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('tag:red') input_filtered_search_keys('tag=red')
expect(page).not_to have_content 'runner-blue' expect(page).not_to have_content 'runner-blue'
expect(page).not_to have_content 'runner-blue' expect(page).not_to have_content 'runner-blue'
...@@ -179,13 +179,13 @@ describe "Admin Runners" do ...@@ -179,13 +179,13 @@ describe "Admin Runners" do
visit admin_runners_path visit admin_runners_path
input_filtered_search_keys('tag:blue') input_filtered_search_keys('tag=blue')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).to have_content 'runner-b-1' expect(page).to have_content 'runner-b-1'
expect(page).not_to have_content 'runner-a-2' expect(page).not_to have_content 'runner-a-2'
input_filtered_search_keys('tag:blue runner-a') input_filtered_search_keys('tag=blue runner-a')
expect(page).to have_content 'runner-a-1' expect(page).to have_content 'runner-a-1'
expect(page).not_to have_content 'runner-b-1' expect(page).not_to have_content 'runner-b-1'
......
...@@ -628,7 +628,7 @@ describe 'Issue Boards', :js do ...@@ -628,7 +628,7 @@ describe 'Issue Boards', :js do
end end
def set_filter(type, text) def set_filter(type, text)
find('.filtered-search').native.send_keys("#{type}:#{text}") find('.filtered-search').native.send_keys("#{type}=#{text}")
end end
def submit_filter def submit_filter
......
...@@ -211,7 +211,7 @@ describe 'Issue Boards add issue modal filtering', :js do ...@@ -211,7 +211,7 @@ describe 'Issue Boards add issue modal filtering', :js do
end end
def set_filter(type, text = '') def set_filter(type, text = '')
find('.add-issues-modal .filtered-search').native.send_keys("#{type}:#{text}") find('.add-issues-modal .filtered-search').native.send_keys("#{type}=#{text}")
end end
def submit_filter def submit_filter
......
...@@ -28,14 +28,14 @@ describe 'Dashboard Issues filtering', :js do ...@@ -28,14 +28,14 @@ describe 'Dashboard Issues filtering', :js do
context 'filtering by milestone' do context 'filtering by milestone' do
it 'shows all issues with no milestone' do it 'shows all issues with no milestone' do
input_filtered_search("milestone:none") input_filtered_search("milestone=none")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
it 'shows all issues with the selected milestone' do it 'shows all issues with the selected milestone' do
input_filtered_search("milestone:%\"#{milestone.title}\"") input_filtered_search("milestone=%\"#{milestone.title}\"")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
...@@ -63,7 +63,7 @@ describe 'Dashboard Issues filtering', :js do ...@@ -63,7 +63,7 @@ describe 'Dashboard Issues filtering', :js do
let!(:label_link) { create(:label_link, label: label, target: issue) } let!(:label_link) { create(:label_link, label: label, target: issue) }
it 'shows all issues with the selected label' do it 'shows all issues with the selected label' do
input_filtered_search("label:~#{label.title}") input_filtered_search("label=~#{label.title}")
page.within 'ul.content-list' do page.within 'ul.content-list' do
expect(page).to have_content issue.title expect(page).to have_content issue.title
......
...@@ -30,7 +30,7 @@ RSpec.describe 'Dashboard Issues' do ...@@ -30,7 +30,7 @@ RSpec.describe 'Dashboard Issues' do
it 'shows issues when current user is author', :js do it 'shows issues when current user is author', :js do
reset_filters reset_filters
input_filtered_search("author:#{current_user.to_reference}") input_filtered_search("author=#{current_user.to_reference}")
expect(page).to have_content(authored_issue.title) expect(page).to have_content(authored_issue.title)
expect(page).to have_content(authored_issue_on_public_project.title) expect(page).to have_content(authored_issue_on_public_project.title)
......
...@@ -107,7 +107,7 @@ describe 'Dashboard Merge Requests' do ...@@ -107,7 +107,7 @@ describe 'Dashboard Merge Requests' do
it 'shows authored merge requests', :js do it 'shows authored merge requests', :js do
reset_filters reset_filters
input_filtered_search("author:#{current_user.to_reference}") input_filtered_search("author=#{current_user.to_reference}")
expect(page).to have_content(authored_merge_request.title) expect(page).to have_content(authored_merge_request.title)
expect(page).to have_content(authored_merge_request_from_fork.title) expect(page).to have_content(authored_merge_request_from_fork.title)
...@@ -120,7 +120,7 @@ describe 'Dashboard Merge Requests' do ...@@ -120,7 +120,7 @@ describe 'Dashboard Merge Requests' do
it 'shows labeled merge requests', :js do it 'shows labeled merge requests', :js do
reset_filters reset_filters
input_filtered_search("label:#{label.name}") input_filtered_search("label=#{label.name}")
expect(page).to have_content(labeled_merge_request.title) expect(page).to have_content(labeled_merge_request.title)
......
...@@ -48,7 +48,7 @@ describe 'Group issues page' do ...@@ -48,7 +48,7 @@ describe 'Group issues page' do
let(:user2) { user_outside_group } let(:user2) { user_outside_group }
it 'filters by only group users' do it 'filters by only group users' do
filtered_search.set('assignee:') filtered_search.set('assignee=')
expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name) expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name) expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
......
...@@ -52,7 +52,7 @@ describe 'Group merge requests page' do ...@@ -52,7 +52,7 @@ describe 'Group merge requests page' do
let(:user2) { user_outside_group } let(:user2) { user_outside_group }
it 'filters by assignee only group users' do it 'filters by assignee only group users' do
filtered_search.set('assignee:') filtered_search.set('assignee=')
expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name) expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name) expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
......
...@@ -20,13 +20,13 @@ describe 'Dropdown assignee', :js do ...@@ -20,13 +20,13 @@ describe 'Dropdown assignee', :js do
describe 'behavior' do describe 'behavior' do
it 'loads all the assignees when opened' do it 'loads all the assignees when opened' do
input_filtered_search('assignee:', submit: false, extra_space: false) input_filtered_search('assignee=', submit: false, extra_space: false)
expect_filtered_search_dropdown_results(filter_dropdown, 2) expect_filtered_search_dropdown_results(filter_dropdown, 2)
end end
it 'shows current user at top of dropdown' do it 'shows current user at top of dropdown' do
input_filtered_search('assignee:', submit: false, extra_space: false) input_filtered_search('assignee=', submit: false, extra_space: false)
expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name) expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
end end
...@@ -35,7 +35,7 @@ describe 'Dropdown assignee', :js do ...@@ -35,7 +35,7 @@ describe 'Dropdown assignee', :js do
describe 'selecting from dropdown without Ajax call' do describe 'selecting from dropdown without Ajax call' do
before do before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests! Gitlab::Testing::RequestBlockerMiddleware.block_requests!
input_filtered_search('assignee:', submit: false, extra_space: false) input_filtered_search('assignee=', submit: false, extra_space: false)
end end
after do after do
......
...@@ -20,13 +20,13 @@ describe 'Dropdown author', :js do ...@@ -20,13 +20,13 @@ describe 'Dropdown author', :js do
describe 'behavior' do describe 'behavior' do
it 'loads all the authors when opened' do it 'loads all the authors when opened' do
input_filtered_search('author:', submit: false, extra_space: false) input_filtered_search('author=', submit: false, extra_space: false)
expect_filtered_search_dropdown_results(filter_dropdown, 2) expect_filtered_search_dropdown_results(filter_dropdown, 2)
end end
it 'shows current user at top of dropdown' do it 'shows current user at top of dropdown' do
input_filtered_search('author:', submit: false, extra_space: false) input_filtered_search('author=', submit: false, extra_space: false)
expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name) expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
end end
...@@ -35,7 +35,7 @@ describe 'Dropdown author', :js do ...@@ -35,7 +35,7 @@ describe 'Dropdown author', :js do
describe 'selecting from dropdown without Ajax call' do describe 'selecting from dropdown without Ajax call' do
before do before do
Gitlab::Testing::RequestBlockerMiddleware.block_requests! Gitlab::Testing::RequestBlockerMiddleware.block_requests!
input_filtered_search('author:', submit: false, extra_space: false) input_filtered_search('author=', submit: false, extra_space: false)
end end
after do after do
......
...@@ -27,14 +27,14 @@ describe 'Dropdown base', :js do ...@@ -27,14 +27,14 @@ describe 'Dropdown base', :js do
it 'shows loading indicator when opened' do it 'shows loading indicator when opened' do
slow_requests do slow_requests do
# We aren't using `input_filtered_search` because we want to see the loading indicator # We aren't using `input_filtered_search` because we want to see the loading indicator
filtered_search.set('assignee:') filtered_search.set('assignee=')
expect(page).to have_css("#{js_dropdown_assignee} .filter-dropdown-loading", visible: true) expect(page).to have_css("#{js_dropdown_assignee} .filter-dropdown-loading", visible: true)
end end
end end
it 'hides loading indicator when loaded' do it 'hides loading indicator when loaded' do
input_filtered_search('assignee:', submit: false, extra_space: false) input_filtered_search('assignee=', submit: false, extra_space: false)
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading') expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end end
...@@ -42,7 +42,7 @@ describe 'Dropdown base', :js do ...@@ -42,7 +42,7 @@ describe 'Dropdown base', :js do
describe 'caching requests' do describe 'caching requests' do
it 'caches requests after the first load' do it 'caches requests after the first load' do
input_filtered_search('assignee:', submit: false, extra_space: false) input_filtered_search('assignee=', submit: false, extra_space: false)
initial_size = dropdown_assignee_size initial_size = dropdown_assignee_size
expect(initial_size).to be > 0 expect(initial_size).to be > 0
...@@ -50,7 +50,7 @@ describe 'Dropdown base', :js do ...@@ -50,7 +50,7 @@ describe 'Dropdown base', :js do
new_user = create(:user) new_user = create(:user)
project.add_maintainer(new_user) project.add_maintainer(new_user)
find('.filtered-search-box .clear-search').click find('.filtered-search-box .clear-search').click
input_filtered_search('assignee:', submit: false, extra_space: false) input_filtered_search('assignee=', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(initial_size) expect(dropdown_assignee_size).to eq(initial_size)
end end
......
...@@ -26,8 +26,8 @@ describe 'Dropdown emoji', :js do ...@@ -26,8 +26,8 @@ describe 'Dropdown emoji', :js do
end end
describe 'behavior' do describe 'behavior' do
it 'does not open when the search bar has my-reaction:' do it 'does not open when the search bar has my-reaction=' do
filtered_search.set('my-reaction:') filtered_search.set('my-reaction=')
expect(page).not_to have_css(js_dropdown_emoji) expect(page).not_to have_css(js_dropdown_emoji)
end end
...@@ -42,20 +42,20 @@ describe 'Dropdown emoji', :js do ...@@ -42,20 +42,20 @@ describe 'Dropdown emoji', :js do
end end
describe 'behavior' do describe 'behavior' do
it 'opens when the search bar has my-reaction:' do it 'opens when the search bar has my-reaction=' do
filtered_search.set('my-reaction:') filtered_search.set('my-reaction=')
expect(page).to have_css(js_dropdown_emoji, visible: true) expect(page).to have_css(js_dropdown_emoji, visible: true)
end end
it 'loads all the emojis when opened' do it 'loads all the emojis when opened' do
input_filtered_search('my-reaction:', submit: false, extra_space: false) input_filtered_search('my-reaction=', submit: false, extra_space: false)
expect_filtered_search_dropdown_results(filter_dropdown, 3) expect_filtered_search_dropdown_results(filter_dropdown, 3)
end end
it 'shows the most populated emoji at top of dropdown' do it 'shows the most populated emoji at top of dropdown' do
input_filtered_search('my-reaction:', submit: false, extra_space: false) input_filtered_search('my-reaction=', submit: false, extra_space: false)
expect(first("#{js_dropdown_emoji} .filter-dropdown li")).to have_content(award_emoji_star.name) expect(first("#{js_dropdown_emoji} .filter-dropdown li")).to have_content(award_emoji_star.name)
end end
......
...@@ -9,11 +9,16 @@ describe 'Dropdown hint', :js do ...@@ -9,11 +9,16 @@ describe 'Dropdown hint', :js do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') } let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_hint) { '#js-dropdown-hint' } let(:js_dropdown_hint) { '#js-dropdown-hint' }
let(:js_dropdown_operator) { '#js-dropdown-operator' }
def click_hint(text) def click_hint(text)
find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click
end end
def click_operator(op)
find("#js-dropdown-operator .filter-dropdown .filter-dropdown-item[data-value='#{op}']").click
end
before do before do
project.add_maintainer(user) project.add_maintainer(user)
create(:issue, project: project) create(:issue, project: project)
...@@ -27,7 +32,7 @@ describe 'Dropdown hint', :js do ...@@ -27,7 +32,7 @@ describe 'Dropdown hint', :js do
it 'does not exist my-reaction dropdown item' do it 'does not exist my-reaction dropdown item' do
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).not_to have_content('my-reaction') expect(page).not_to have_content('My-reaction')
end end
end end
...@@ -54,15 +59,6 @@ describe 'Dropdown hint', :js do ...@@ -54,15 +59,6 @@ describe 'Dropdown hint', :js do
end end
describe 'filtering' do describe 'filtering' do
it 'does not filter `Press Enter or click to search`' do
filtered_search.set('randomtext')
hint_dropdown = find(js_dropdown_hint)
expect(hint_dropdown).to have_content('Press Enter or click to search')
expect(hint_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 0)
end
it 'filters with text' do it 'filters with text' do
filtered_search.set('a') filtered_search.set('a')
...@@ -76,21 +72,27 @@ describe 'Dropdown hint', :js do ...@@ -76,21 +72,27 @@ describe 'Dropdown hint', :js do
end end
it 'opens the token dropdown when you click on it' do it 'opens the token dropdown when you click on it' do
click_hint('author') click_hint('Author')
expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css(js_dropdown_operator, visible: true)
click_operator('=')
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css(js_dropdown_operator, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true) expect(page).to have_css('#js-dropdown-author', visible: true)
expect_tokens([{ name: 'Author' }]) expect_tokens([{ name: 'Author', operator: '=' }])
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
end end
describe 'reselecting from dropdown' do describe 'reselecting from dropdown' do
it 'reuses existing token text' do it 'reuses existing token text' do
filtered_search.send_keys('author:') filtered_search.send_keys('author')
filtered_search.send_keys(:backspace) filtered_search.send_keys(:backspace)
filtered_search.send_keys(:backspace) filtered_search.send_keys(:backspace)
click_hint('author') click_hint('Author')
expect_tokens([{ name: 'Author' }]) expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty expect_filtered_search_input_empty
......
...@@ -21,7 +21,7 @@ describe 'Dropdown label', :js do ...@@ -21,7 +21,7 @@ describe 'Dropdown label', :js do
describe 'behavior' do describe 'behavior' do
it 'loads all the labels when opened' do it 'loads all the labels when opened' do
create(:label, project: project, title: 'bug-label') create(:label, project: project, title: 'bug-label')
filtered_search.set('label:') filtered_search.set('label=')
expect_filtered_search_dropdown_results(filter_dropdown, 1) expect_filtered_search_dropdown_results(filter_dropdown, 1)
end end
......
...@@ -23,7 +23,7 @@ describe 'Dropdown milestone', :js do ...@@ -23,7 +23,7 @@ describe 'Dropdown milestone', :js do
describe 'behavior' do describe 'behavior' do
before do before do
filtered_search.set('milestone:') filtered_search.set('milestone=')
end end
it 'loads all the milestones when opened' do it 'loads all the milestones when opened' do
......
...@@ -23,7 +23,7 @@ describe 'Dropdown release', :js do ...@@ -23,7 +23,7 @@ describe 'Dropdown release', :js do
describe 'behavior' do describe 'behavior' do
before do before do
filtered_search.set('release:') filtered_search.set('release=')
end end
it 'loads all the releases when opened' do it 'loads all the releases when opened' do
......
...@@ -41,8 +41,8 @@ describe 'Recent searches', :js do ...@@ -41,8 +41,8 @@ describe 'Recent searches', :js do
items = all('.filtered-search-history-dropdown-item', visible: false, count: 2) items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
expect(items[0].text).to eq('label: ~qux garply') expect(items[0].text).to eq('label: = ~qux garply')
expect(items[1].text).to eq('label: ~foo bar') expect(items[1].text).to eq('label: = ~foo bar')
end end
it 'saved recent searches are restored last on the list' do it 'saved recent searches are restored last on the list' do
......
...@@ -34,7 +34,7 @@ describe 'Search bar', :js do ...@@ -34,7 +34,7 @@ describe 'Search bar', :js do
it 'selects item' do it 'selects item' do
filtered_search.native.send_keys(:down, :down, :enter) filtered_search.native.send_keys(:down, :down, :enter)
expect_tokens([author_token]) expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty expect_filtered_search_input_empty
end end
end end
...@@ -78,7 +78,7 @@ describe 'Search bar', :js do ...@@ -78,7 +78,7 @@ describe 'Search bar', :js do
filtered_search.click filtered_search.click
original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size
filtered_search.set('author') filtered_search.set('autho')
expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1) expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
......
...@@ -36,8 +36,9 @@ describe 'Visual tokens', :js do ...@@ -36,8 +36,9 @@ describe 'Visual tokens', :js do
describe 'editing a single token' do describe 'editing a single token' do
before do before do
input_filtered_search('author:@root assignee:none', submit: false) input_filtered_search('author=@root assignee=none', submit: false)
first('.tokens-container .filtered-search-token').click first('.tokens-container .filtered-search-token').click
wait_for_requests
end end
it 'opens author dropdown' do it 'opens author dropdown' do
...@@ -76,8 +77,8 @@ describe 'Visual tokens', :js do ...@@ -76,8 +77,8 @@ describe 'Visual tokens', :js do
describe 'editing multiple tokens' do describe 'editing multiple tokens' do
before do before do
input_filtered_search('author:@root assignee:none', submit: false) input_filtered_search('author=@root assignee=none', submit: false)
first('.tokens-container .filtered-search-token').double_click first('.tokens-container .filtered-search-token').click
end end
it 'opens author dropdown' do it 'opens author dropdown' do
...@@ -85,27 +86,33 @@ describe 'Visual tokens', :js do ...@@ -85,27 +86,33 @@ describe 'Visual tokens', :js do
end end
it 'opens assignee dropdown' do it 'opens assignee dropdown' do
find('.tokens-container .filtered-search-token', text: 'Assignee').double_click find('.tokens-container .filtered-search-token', text: 'Assignee').click
expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(page).to have_css('#js-dropdown-assignee', visible: true)
end end
end end
describe 'editing a search term while editing another filter token' do describe 'editing a search term while editing another filter token' do
before do before do
input_filtered_search('author assignee:', submit: false) input_filtered_search('foo assignee=', submit: false)
first('.tokens-container .filtered-search-term').double_click first('.tokens-container .filtered-search-term').click
end end
it 'opens author dropdown' do it 'opens author dropdown' do
find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'Author').click
expect(page).to have_css('#js-dropdown-operator', visible: true)
expect(page).to have_css('#js-dropdown-author', visible: false)
find('#js-dropdown-operator .filter-dropdown .filter-dropdown-item[data-value="="]').click
expect(page).to have_css('#js-dropdown-operator', visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true) expect(page).to have_css('#js-dropdown-author', visible: true)
end end
end end
describe 'add new token after editing existing token' do describe 'add new token after editing existing token' do
before do before do
input_filtered_search('author:@root assignee:none', submit: false) input_filtered_search('author=@root assignee=none', submit: false)
first('.tokens-container .filtered-search-token').double_click first('.tokens-container .filtered-search-token').double_click
filtered_search.send_keys(' ') filtered_search.send_keys(' ')
end end
...@@ -116,7 +123,7 @@ describe 'Visual tokens', :js do ...@@ -116,7 +123,7 @@ describe 'Visual tokens', :js do
end end
it 'opens token dropdown' do it 'opens token dropdown' do
filtered_search.send_keys('author:') filtered_search.send_keys('author=')
expect(page).to have_css('#js-dropdown-author', visible: true) expect(page).to have_css('#js-dropdown-author', visible: true)
end end
...@@ -124,7 +131,7 @@ describe 'Visual tokens', :js do ...@@ -124,7 +131,7 @@ describe 'Visual tokens', :js do
describe 'visual tokens' do describe 'visual tokens' do
it 'creates visual token' do it 'creates visual token' do
filtered_search.send_keys('author:@thomas ') filtered_search.send_keys('author=@thomas ')
token = page.all('.tokens-container .filtered-search-token')[1] token = page.all('.tokens-container .filtered-search-token')[1]
expect(token.find('.name').text).to eq('Author') expect(token.find('.name').text).to eq('Author')
...@@ -133,7 +140,7 @@ describe 'Visual tokens', :js do ...@@ -133,7 +140,7 @@ describe 'Visual tokens', :js do
end end
it 'does not tokenize incomplete token' do it 'does not tokenize incomplete token' do
filtered_search.send_keys('author:') filtered_search.send_keys('author=')
find('body').click find('body').click
token = page.all('.tokens-container .js-visual-token')[1] token = page.all('.tokens-container .js-visual-token')[1]
...@@ -145,7 +152,7 @@ describe 'Visual tokens', :js do ...@@ -145,7 +152,7 @@ describe 'Visual tokens', :js do
describe 'search using incomplete visual tokens' do describe 'search using incomplete visual tokens' do
before do before do
input_filtered_search('author:@root assignee:none', extra_space: false) input_filtered_search('author=@root assignee=none', extra_space: false)
end end
it 'tokenizes the search term to complete visual token' do it 'tokenizes the search term to complete visual token' do
......
...@@ -70,7 +70,7 @@ describe 'Labels Hierarchy', :js do ...@@ -70,7 +70,7 @@ describe 'Labels Hierarchy', :js do
end end
it 'does not filter by descendant group labels' do it 'does not filter by descendant group labels' do
filtered_search.set("label:") filtered_search.set("label=")
wait_for_requests wait_for_requests
...@@ -134,7 +134,7 @@ describe 'Labels Hierarchy', :js do ...@@ -134,7 +134,7 @@ describe 'Labels Hierarchy', :js do
end end
it 'does not filter by descendant group project labels' do it 'does not filter by descendant group project labels' do
filtered_search.set("label:") filtered_search.set("label=")
wait_for_requests wait_for_requests
...@@ -227,7 +227,7 @@ describe 'Labels Hierarchy', :js do ...@@ -227,7 +227,7 @@ describe 'Labels Hierarchy', :js do
it_behaves_like 'filtering by ancestor labels for projects' it_behaves_like 'filtering by ancestor labels for projects'
it 'does not filter by descendant group labels' do it 'does not filter by descendant group labels' do
filtered_search.set("label:") filtered_search.set("label=")
wait_for_requests wait_for_requests
......
...@@ -23,7 +23,7 @@ describe 'Merge Requests > Filters generic behavior', :js do ...@@ -23,7 +23,7 @@ describe 'Merge Requests > Filters generic behavior', :js do
context 'when filtered by a label' do context 'when filtered by a label' do
before do before do
input_filtered_search('label:~bug') input_filtered_search('label=~bug')
end end
describe 'state tabs' do describe 'state tabs' do
......
...@@ -18,7 +18,7 @@ describe 'Merge Requests > User filters by assignees', :js do ...@@ -18,7 +18,7 @@ describe 'Merge Requests > User filters by assignees', :js do
context 'filtering by assignee:none' do context 'filtering by assignee:none' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('assignee:none') input_filtered_search('assignee=none')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).not_to have_content 'Bugfix1' expect(page).not_to have_content 'Bugfix1'
...@@ -26,9 +26,9 @@ describe 'Merge Requests > User filters by assignees', :js do ...@@ -26,9 +26,9 @@ describe 'Merge Requests > User filters by assignees', :js do
end end
end end
context 'filtering by assignee:@username' do context 'filtering by assignee=@username' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search("assignee:@#{user.username}") input_filtered_search("assignee=@#{user.username}")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix1' expect(page).to have_content 'Bugfix1'
......
...@@ -22,7 +22,7 @@ describe 'Merge Requests > User filters by labels', :js do ...@@ -22,7 +22,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:none' do context 'filtering by label:none' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('label:none') input_filtered_search('label=none')
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).not_to have_content 'Bugfix1' expect(page).not_to have_content 'Bugfix1'
...@@ -32,7 +32,7 @@ describe 'Merge Requests > User filters by labels', :js do ...@@ -32,7 +32,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:~enhancement' do context 'filtering by label:~enhancement' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('label:~enhancement') input_filtered_search('label=~enhancement')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix2' expect(page).to have_content 'Bugfix2'
...@@ -42,7 +42,7 @@ describe 'Merge Requests > User filters by labels', :js do ...@@ -42,7 +42,7 @@ describe 'Merge Requests > User filters by labels', :js do
context 'filtering by label:~enhancement and label:~bug' do context 'filtering by label:~enhancement and label:~bug' do
it 'applies the filters' do it 'applies the filters' do
input_filtered_search('label:~bug label:~enhancement') input_filtered_search('label=~bug label=~enhancement')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix2' expect(page).to have_content 'Bugfix2'
......
...@@ -18,14 +18,14 @@ describe 'Merge Requests > User filters by milestones', :js do ...@@ -18,14 +18,14 @@ describe 'Merge Requests > User filters by milestones', :js do
end end
it 'filters by no milestone' do it 'filters by no milestone' do
input_filtered_search('milestone:none') input_filtered_search('milestone=none')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
it 'filters by a specific milestone' do it 'filters by a specific milestone' do
input_filtered_search("milestone:%'#{milestone.title}'") input_filtered_search("milestone=%'#{milestone.title}'")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
...@@ -33,7 +33,7 @@ describe 'Merge Requests > User filters by milestones', :js do ...@@ -33,7 +33,7 @@ describe 'Merge Requests > User filters by milestones', :js do
describe 'filters by upcoming milestone' do describe 'filters by upcoming milestone' do
it 'does not show merge requests with no expiry' do it 'does not show merge requests with no expiry' do
input_filtered_search('milestone:upcoming') input_filtered_search('milestone=upcoming')
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).to have_css('.merge-request', count: 0) expect(page).to have_css('.merge-request', count: 0)
...@@ -43,7 +43,7 @@ describe 'Merge Requests > User filters by milestones', :js do ...@@ -43,7 +43,7 @@ describe 'Merge Requests > User filters by milestones', :js do
let(:milestone) { create(:milestone, project: project, due_date: Date.tomorrow) } let(:milestone) { create(:milestone, project: project, due_date: Date.tomorrow) }
it 'shows merge requests' do it 'shows merge requests' do
input_filtered_search('milestone:upcoming') input_filtered_search('milestone=upcoming')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
...@@ -54,7 +54,7 @@ describe 'Merge Requests > User filters by milestones', :js do ...@@ -54,7 +54,7 @@ describe 'Merge Requests > User filters by milestones', :js do
let(:milestone) { create(:milestone, project: project, due_date: Date.yesterday) } let(:milestone) { create(:milestone, project: project, due_date: Date.yesterday) }
it 'does not show any merge requests' do it 'does not show any merge requests' do
input_filtered_search('milestone:upcoming') input_filtered_search('milestone=upcoming')
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).to have_css('.merge-request', count: 0) expect(page).to have_css('.merge-request', count: 0)
......
...@@ -20,7 +20,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do ...@@ -20,7 +20,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
describe 'filtering by label:~"Won\'t fix" and assignee:~bug' do describe 'filtering by label:~"Won\'t fix" and assignee:~bug' do
it 'applies the filters' do it 'applies the filters' do
input_filtered_search("label:~\"Won't fix\" assignee:@#{user.username}") input_filtered_search("label=~\"Won't fix\" assignee=@#{user.username}")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix2' expect(page).to have_content 'Bugfix2'
...@@ -30,7 +30,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do ...@@ -30,7 +30,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
describe 'filtering by text, author, assignee, milestone, and label' do describe 'filtering by text, author, assignee, milestone, and label' do
it 'filters by text, author, assignee, milestone, and label' do it 'filters by text, author, assignee, milestone, and label' do
input_filtered_search_keys("author:@#{user.username} assignee:@#{user.username} milestone:%\"v1.1\" label:~\"Won't fix\" Bug") input_filtered_search_keys("author=@#{user.username} assignee=@#{user.username} milestone=%\"v1.1\" label=~\"Won't fix\" Bug")
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content 'Bugfix2' expect(page).to have_content 'Bugfix2'
......
...@@ -17,7 +17,7 @@ describe 'Merge Requests > User filters by target branch', :js do ...@@ -17,7 +17,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:master' do context 'filtering by target-branch:master' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('target-branch:master') input_filtered_search('target-branch=master')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content mr1.title expect(page).to have_content mr1.title
...@@ -27,7 +27,7 @@ describe 'Merge Requests > User filters by target branch', :js do ...@@ -27,7 +27,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:merged-target' do context 'filtering by target-branch:merged-target' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('target-branch:merged-target') input_filtered_search('target-branch=merged-target')
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).not_to have_content mr1.title expect(page).not_to have_content mr1.title
...@@ -37,7 +37,7 @@ describe 'Merge Requests > User filters by target branch', :js do ...@@ -37,7 +37,7 @@ describe 'Merge Requests > User filters by target branch', :js do
context 'filtering by target-branch:feature' do context 'filtering by target-branch:feature' do
it 'applies the filter' do it 'applies the filter' do
input_filtered_search('target-branch:feature') input_filtered_search('target-branch=feature')
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0) expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).not_to have_content mr1.title expect(page).not_to have_content mr1.title
......
...@@ -124,6 +124,7 @@ describe('Filtered Search Token Keys', () => { ...@@ -124,6 +124,7 @@ describe('Filtered Search Token Keys', () => {
const condition = new FilteredSearchTokenKeys([], [], conditions).searchByConditionKeyValue( const condition = new FilteredSearchTokenKeys([], [], conditions).searchByConditionKeyValue(
null, null,
null, null,
null,
); );
expect(condition).toBeNull(); expect(condition).toBeNull();
...@@ -132,6 +133,7 @@ describe('Filtered Search Token Keys', () => { ...@@ -132,6 +133,7 @@ describe('Filtered Search Token Keys', () => {
it('should return condition when found by tokenKey and value', () => { it('should return condition when found by tokenKey and value', () => {
const result = new FilteredSearchTokenKeys([], [], conditions).searchByConditionKeyValue( const result = new FilteredSearchTokenKeys([], [], conditions).searchByConditionKeyValue(
conditions[0].tokenKey, conditions[0].tokenKey,
conditions[0].operator,
conditions[0].value, conditions[0].value,
); );
......
...@@ -398,14 +398,21 @@ describe('DropLab DropDown', function() { ...@@ -398,14 +398,21 @@ describe('DropLab DropDown', function() {
describe('render', function() { describe('render', function() {
beforeEach(function() { beforeEach(function() {
this.list = { querySelector: () => {}, dispatchEvent: () => {} };
this.dropdown = { renderChildren: () => {}, list: this.list };
this.renderableList = {}; this.renderableList = {};
this.list = {
querySelector: q => {
if (q === '.filter-dropdown-loading') {
return false;
}
return this.renderableList;
},
dispatchEvent: () => {},
};
this.dropdown = { renderChildren: () => {}, list: this.list };
this.data = [0, 1]; this.data = [0, 1];
this.customEvent = {}; this.customEvent = {};
spyOn(this.dropdown, 'renderChildren').and.callFake(data => data); spyOn(this.dropdown, 'renderChildren').and.callFake(data => data);
spyOn(this.list, 'querySelector').and.returnValue(this.renderableList);
spyOn(this.list, 'dispatchEvent'); spyOn(this.list, 'dispatchEvent');
spyOn(this.data, 'map').and.callThrough(); spyOn(this.data, 'map').and.callThrough();
spyOn(window, 'CustomEvent').and.returnValue(this.customEvent); spyOn(window, 'CustomEvent').and.returnValue(this.customEvent);
......
...@@ -222,7 +222,7 @@ describe('Dropdown Utils', () => { ...@@ -222,7 +222,7 @@ describe('Dropdown Utils', () => {
hasAttribute: () => false, hasAttribute: () => false,
}; };
DropdownUtils.setDataValueIfSelected(null, selected); DropdownUtils.setDataValueIfSelected(null, '=', selected);
expect(FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); expect(FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
}); });
...@@ -233,9 +233,11 @@ describe('Dropdown Utils', () => { ...@@ -233,9 +233,11 @@ describe('Dropdown Utils', () => {
hasAttribute: () => false, hasAttribute: () => false,
}; };
const result = DropdownUtils.setDataValueIfSelected(null, selected); const result = DropdownUtils.setDataValueIfSelected(null, '=', selected);
const result2 = DropdownUtils.setDataValueIfSelected(null, '!=', selected);
expect(result).toBe(true); expect(result).toBe(true);
expect(result2).toBe(true);
}); });
it('returns false when dataValue does not exist', () => { it('returns false when dataValue does not exist', () => {
...@@ -243,9 +245,11 @@ describe('Dropdown Utils', () => { ...@@ -243,9 +245,11 @@ describe('Dropdown Utils', () => {
getAttribute: () => null, getAttribute: () => null,
}; };
const result = DropdownUtils.setDataValueIfSelected(null, selected); const result = DropdownUtils.setDataValueIfSelected(null, '=', selected);
const result2 = DropdownUtils.setDataValueIfSelected(null, '!=', selected);
expect(result).toBe(false); expect(result).toBe(false);
expect(result2).toBe(false);
}); });
}); });
...@@ -349,7 +353,7 @@ describe('Dropdown Utils', () => { ...@@ -349,7 +353,7 @@ describe('Dropdown Utils', () => {
beforeEach(() => { beforeEach(() => {
loadFixtures(issueListFixture); loadFixtures(issueListFixture);
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user'); authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term'); const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
const tokensContainer = document.querySelector('.tokens-container'); const tokensContainer = document.querySelector('.tokens-container');
...@@ -364,7 +368,7 @@ describe('Dropdown Utils', () => { ...@@ -364,7 +368,7 @@ describe('Dropdown Utils', () => {
const searchQuery = DropdownUtils.getSearchQuery(); const searchQuery = DropdownUtils.getSearchQuery();
expect(searchQuery).toBe(' search term author:original dance'); expect(searchQuery).toBe(' search term author:=original dance');
}); });
}); });
}); });
...@@ -27,7 +27,7 @@ describe('Filtered Search Dropdown Manager', () => { ...@@ -27,7 +27,7 @@ describe('Filtered Search Dropdown Manager', () => {
describe('input has no existing value', () => { describe('input has no existing value', () => {
it('should add just tokenName', () => { it('should add just tokenName', () => {
FilteredSearchDropdownManager.addWordToInput('milestone'); FilteredSearchDropdownManager.addWordToInput({ tokenName: 'milestone' });
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
...@@ -36,8 +36,8 @@ describe('Filtered Search Dropdown Manager', () => { ...@@ -36,8 +36,8 @@ describe('Filtered Search Dropdown Manager', () => {
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
it('should add tokenName and tokenValue', () => { it('should add tokenName, tokenOperator, and tokenValue', () => {
FilteredSearchDropdownManager.addWordToInput('label'); FilteredSearchDropdownManager.addWordToInput({ tokenName: 'label' });
let token = document.querySelector('.tokens-container .js-visual-token'); let token = document.querySelector('.tokens-container .js-visual-token');
...@@ -45,13 +45,27 @@ describe('Filtered Search Dropdown Manager', () => { ...@@ -45,13 +45,27 @@ describe('Filtered Search Dropdown Manager', () => {
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
FilteredSearchDropdownManager.addWordToInput('label', 'none'); FilteredSearchDropdownManager.addWordToInput({ tokenName: 'label', tokenOperator: '=' });
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('.operator').innerText).toBe('=');
expect(getInputValue()).toBe('');
FilteredSearchDropdownManager.addWordToInput({
tokenName: 'label',
tokenOperator: '=',
tokenValue: 'none',
});
// We have to get that reference again // We have to get that reference again
// Because FilteredSearchDropdownManager deletes the previous token // Because FilteredSearchDropdownManager deletes the previous token
token = document.querySelector('.tokens-container .js-visual-token'); token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.operator').innerText).toBe('=');
expect(token.querySelector('.value').innerText).toBe('none'); expect(token.querySelector('.value').innerText).toBe('none');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
...@@ -60,7 +74,7 @@ describe('Filtered Search Dropdown Manager', () => { ...@@ -60,7 +74,7 @@ describe('Filtered Search Dropdown Manager', () => {
describe('input has existing value', () => { describe('input has existing value', () => {
it('should be able to just add tokenName', () => { it('should be able to just add tokenName', () => {
setInputValue('a'); setInputValue('a');
FilteredSearchDropdownManager.addWordToInput('author'); FilteredSearchDropdownManager.addWordToInput({ tokenName: 'author' });
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
...@@ -70,29 +84,40 @@ describe('Filtered Search Dropdown Manager', () => { ...@@ -70,29 +84,40 @@ describe('Filtered Search Dropdown Manager', () => {
}); });
it('should replace tokenValue', () => { it('should replace tokenValue', () => {
FilteredSearchDropdownManager.addWordToInput('author'); FilteredSearchDropdownManager.addWordToInput({ tokenName: 'author' });
FilteredSearchDropdownManager.addWordToInput({ tokenName: 'author', tokenOperator: '=' });
setInputValue('roo'); setInputValue('roo');
FilteredSearchDropdownManager.addWordToInput(null, '@root'); FilteredSearchDropdownManager.addWordToInput({
tokenName: null,
tokenOperator: '=',
tokenValue: '@root',
});
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author'); expect(token.querySelector('.name').innerText).toBe('author');
expect(token.querySelector('.operator').innerText).toBe('=');
expect(token.querySelector('.value').innerText).toBe('@root'); expect(token.querySelector('.value').innerText).toBe('@root');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
it('should add tokenValues containing spaces', () => { it('should add tokenValues containing spaces', () => {
FilteredSearchDropdownManager.addWordToInput('label'); FilteredSearchDropdownManager.addWordToInput({ tokenName: 'label' });
setInputValue('"test '); setInputValue('"test ');
FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); FilteredSearchDropdownManager.addWordToInput({
tokenName: 'label',
tokenOperator: '=',
tokenValue: '~\'"test me"\'',
});
const token = document.querySelector('.tokens-container .js-visual-token'); const token = document.querySelector('.tokens-container .js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true); expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label'); expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.operator').innerText).toBe('=');
expect(token.querySelector('.value').innerText).toBe('~\'"test me"\''); expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
expect(getInputValue()).toBe(''); expect(getInputValue()).toBe('');
}); });
......
...@@ -201,8 +201,8 @@ describe('Filtered Search Manager', function() { ...@@ -201,8 +201,8 @@ describe('Filtered Search Manager', function() {
it('removes duplicated tokens', done => { it('removes duplicated tokens', done => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
`); `);
spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => { spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
...@@ -234,7 +234,7 @@ describe('Filtered Search Manager', function() { ...@@ -234,7 +234,7 @@ describe('Filtered Search Manager', function() {
it('should not render placeholder when there are tokens and no input', () => { it('should not render placeholder when there are tokens and no input', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
); );
const event = new Event('input'); const event = new Event('input');
...@@ -252,7 +252,7 @@ describe('Filtered Search Manager', function() { ...@@ -252,7 +252,7 @@ describe('Filtered Search Manager', function() {
describe('tokens and no input', () => { describe('tokens and no input', () => {
beforeEach(() => { beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'), FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
); );
}); });
...@@ -306,7 +306,7 @@ describe('Filtered Search Manager', function() { ...@@ -306,7 +306,7 @@ describe('Filtered Search Manager', function() {
it('removes token even when it is already selected', () => { it('removes token even when it is already selected', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none', true),
); );
tokensContainer.querySelector('.js-visual-token .remove-token').click(); tokensContainer.querySelector('.js-visual-token .remove-token').click();
...@@ -319,7 +319,7 @@ describe('Filtered Search Manager', function() { ...@@ -319,7 +319,7 @@ describe('Filtered Search Manager', function() {
spyOn(FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough(); spyOn(FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'), FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none'),
); );
tokensContainer.querySelector('.js-visual-token .remove-token').click(); tokensContainer.querySelector('.js-visual-token .remove-token').click();
}); });
...@@ -338,7 +338,7 @@ describe('Filtered Search Manager', function() { ...@@ -338,7 +338,7 @@ describe('Filtered Search Manager', function() {
beforeEach(() => { beforeEach(() => {
initializeManager(); initializeManager();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true), FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none', true),
); );
}); });
...@@ -424,7 +424,7 @@ describe('Filtered Search Manager', function() { ...@@ -424,7 +424,7 @@ describe('Filtered Search Manager', function() {
}); });
it('Clicking the "x" clear button, clears the input', () => { it('Clicking the "x" clear button, clears the input', () => {
const inputValue = 'label:~bug '; const inputValue = 'label:=~bug';
manager.filteredSearchInput.value = inputValue; manager.filteredSearchInput.value = inputValue;
manager.filteredSearchInput.dispatchEvent(new Event('input')); manager.filteredSearchInput.dispatchEvent(new Event('input'));
......
...@@ -138,6 +138,7 @@ describe('Issues Filtered Search Token Keys', () => { ...@@ -138,6 +138,7 @@ describe('Issues Filtered Search Token Keys', () => {
const conditions = IssuableFilteredSearchTokenKeys.getConditions(); const conditions = IssuableFilteredSearchTokenKeys.getConditions();
const result = IssuableFilteredSearchTokenKeys.searchByConditionKeyValue( const result = IssuableFilteredSearchTokenKeys.searchByConditionKeyValue(
conditions[0].tokenKey, conditions[0].tokenKey,
conditions[0].operator,
conditions[0].value, conditions[0].value,
); );
......
...@@ -10,9 +10,11 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -10,9 +10,11 @@ describe('Filtered Search Visual Tokens', () => {
const tokenNameElement = tokenElement.querySelector('.name'); const tokenNameElement = tokenElement.querySelector('.name');
const tokenValueContainer = tokenElement.querySelector('.value-container'); const tokenValueContainer = tokenElement.querySelector('.value-container');
const tokenValueElement = tokenValueContainer.querySelector('.value'); const tokenValueElement = tokenValueContainer.querySelector('.value');
const tokenOperatorElement = tokenElement.querySelector('.operator');
const tokenType = tokenNameElement.innerText.toLowerCase(); const tokenType = tokenNameElement.innerText.toLowerCase();
const tokenValue = tokenValueElement.innerText; const tokenValue = tokenValueElement.innerText;
const subject = new VisualTokenValue(tokenValue, tokenType); const tokenOperator = tokenOperatorElement.innerText;
const subject = new VisualTokenValue(tokenValue, tokenType, tokenOperator);
return { subject, tokenValueContainer, tokenValueElement }; return { subject, tokenValueContainer, tokenValueElement };
}; };
...@@ -28,8 +30,8 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -28,8 +30,8 @@ describe('Filtered Search Visual Tokens', () => {
`); `);
tokensContainer = document.querySelector('.tokens-container'); tokensContainer = document.querySelector('.tokens-container');
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user'); authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug'); bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '=', '~bug');
}); });
describe('updateUserTokenAppearance', () => { describe('updateUserTokenAppearance', () => {
...@@ -140,10 +142,12 @@ describe('Filtered Search Visual Tokens', () => { ...@@ -140,10 +142,12 @@ describe('Filtered Search Visual Tokens', () => {
const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken( const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
'label', 'label',
'=',
'~doesnotexist', '~doesnotexist',
); );
const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken( const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
'label', 'label',
'=',
'~"some space"', '~"some space"',
); );
......
export default class FilteredSearchSpecHelper { export default class FilteredSearchSpecHelper {
static createFilterVisualTokenHTML(name, value, isSelected) { static createFilterVisualTokenHTML(name, operator, value, isSelected) {
return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML; return FilteredSearchSpecHelper.createFilterVisualToken(name, operator, value, isSelected)
.outerHTML;
} }
static createFilterVisualToken(name, value, isSelected = false) { static createFilterVisualToken(name, operator, value, isSelected = false) {
const li = document.createElement('li'); const li = document.createElement('li');
li.classList.add('js-visual-token', 'filtered-search-token', `search-token-${name}`); li.classList.add('js-visual-token', 'filtered-search-token', `search-token-${name}`);
li.innerHTML = ` li.innerHTML = `
<div class="selectable ${isSelected ? 'selected' : ''}" role="button"> <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
<div class="name">${name}</div> <div class="name">${name}</div>
<div class="operator">${operator}</div>
<div class="value-container"> <div class="value-container">
<div class="value">${value}</div> <div class="value">${value}</div>
<div class="remove-token" role="button"> <div class="remove-token" role="button">
...@@ -30,6 +32,15 @@ export default class FilteredSearchSpecHelper { ...@@ -30,6 +32,15 @@ export default class FilteredSearchSpecHelper {
`; `;
} }
static createNameOperatorFilterVisualTokenHTML(name, operator) {
return `
<li class="js-visual-token filtered-search-token">
<div class="name">${name}</div>
<div class="operator">${operator}</div>
</li>
`;
}
static createSearchVisualToken(name) { static createSearchVisualToken(name) {
const li = document.createElement('li'); const li = document.createElement('li');
li.classList.add('js-visual-token', 'filtered-search-term'); li.classList.add('js-visual-token', 'filtered-search-term');
......
...@@ -26,7 +26,7 @@ module FilteredSearchHelpers ...@@ -26,7 +26,7 @@ module FilteredSearchHelpers
# Select a label clicking in the search dropdown instead # Select a label clicking in the search dropdown instead
# of entering label names on the input. # of entering label names on the input.
def select_label_on_dropdown(label_title) def select_label_on_dropdown(label_title)
input_filtered_search("label:", submit: false) input_filtered_search("label=", submit: false)
within('#js-dropdown-label') do within('#js-dropdown-label') do
wait_for_requests wait_for_requests
...@@ -71,7 +71,7 @@ module FilteredSearchHelpers ...@@ -71,7 +71,7 @@ module FilteredSearchHelpers
end end
def init_label_search def init_label_search
filtered_search.set('label:') filtered_search.set('label=')
# This ensures the dropdown is shown # This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading') expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
end end
...@@ -90,6 +90,7 @@ module FilteredSearchHelpers ...@@ -90,6 +90,7 @@ module FilteredSearchHelpers
el = token_elements[index] el = token_elements[index]
expect(el.find('.name')).to have_content(token[:name]) expect(el.find('.name')).to have_content(token[:name])
expect(el.find('.operator')).to have_content(token[:operator]) if token[:operator].present?
expect(el.find('.value')).to have_content(token[:value]) if token[:value].present? expect(el.find('.value')).to have_content(token[:value]) if token[:value].present?
# gl-emoji content is blank when the emoji unicode is not supported # gl-emoji content is blank when the emoji unicode is not supported
...@@ -101,8 +102,8 @@ module FilteredSearchHelpers ...@@ -101,8 +102,8 @@ module FilteredSearchHelpers
end end
end end
def create_token(token_name, token_value = nil, symbol = nil) def create_token(token_name, token_value = nil, symbol = nil, token_operator = '=')
{ name: token_name, value: "#{symbol}#{token_value}" } { name: token_name, operator: token_operator, value: "#{symbol}#{token_value}" }
end end
def author_token(author_name = nil) def author_token(author_name = nil)
...@@ -113,9 +114,9 @@ module FilteredSearchHelpers ...@@ -113,9 +114,9 @@ module FilteredSearchHelpers
create_token('Assignee', assignee_name) create_token('Assignee', assignee_name)
end end
def milestone_token(milestone_name = nil, has_symbol = true) def milestone_token(milestone_name = nil, has_symbol = true, operator = '=')
symbol = has_symbol ? '%' : nil symbol = has_symbol ? '%' : nil
create_token('Milestone', milestone_name, symbol) create_token('Milestone', milestone_name, symbol, operator)
end end
def release_token(release_tag = nil) def release_token(release_tag = nil)
......
...@@ -13,7 +13,7 @@ shared_examples 'issuable user dropdown behaviors' do ...@@ -13,7 +13,7 @@ shared_examples 'issuable user dropdown behaviors' do
it 'only includes members of the project/group' do it 'only includes members of the project/group' do
visit issuables_path visit issuables_path
filtered_search.set("#{dropdown}:") filtered_search.set("#{dropdown}=")
expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name) expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name) expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment