Skip to content
Snippets Groups Projects
Commit bddd09c4 authored by Jacob Schatz's avatar Jacob Schatz
Browse files

Merge branch 'issue-search-token-position' into 'master'

Filtered search input click back at token

See merge request !8617
parents e8552366 e75c20fc
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -9,7 +9,7 @@
this.config = {
droplabFilter: {
template: 'hint',
filterFunction: gl.DropdownUtils.filterHint,
filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
},
};
}
Loading
Loading
Loading
Loading
@@ -15,7 +15,7 @@
loadingTemplate: this.loadingTemplate,
},
droplabFilter: {
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol),
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
},
};
}
Loading
Loading
Loading
Loading
@@ -37,7 +37,7 @@
}
 
getSearchInput() {
const query = this.input.value.trim();
const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
 
return lastToken.value || '';
Loading
Loading
Loading
Loading
@@ -20,17 +20,15 @@
return escapedText;
}
 
static filterWithSymbol(filterSymbol, item, query) {
static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input);
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
 
if (lastToken !== searchToken) {
const title = updatedItem.title.toLowerCase();
let value = lastToken.value.toLowerCase();
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
value = value.slice(1);
}
value = value.replace(/"(.*?)"/g, str => str.slice(1).slice(0, -1));
 
// Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
Loading
Loading
@@ -44,8 +42,9 @@
return updatedItem;
}
 
static filterHint(item, query) {
static filterHint(input, item) {
const updatedItem = item;
const query = gl.DropdownUtils.getSearchInput(input);
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
lastToken = lastToken.key || lastToken || '';
 
Loading
Loading
@@ -72,6 +71,48 @@
// Return boolean based on whether it was set
return dataValue !== null;
}
static getSearchInput(filteredSearchInput) {
const inputValue = filteredSearchInput.value;
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
return inputValue.slice(0, right);
}
static getInputSelectionPosition(input) {
const selectionStart = input.selectionStart;
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// This helps with matching the beginning & end of a token:key
inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
let right = inputValue.slice(selectionStart).search(/\s/);
if (right >= 0) {
right += selectionStart;
} else if (right < 0) {
right = inputValue.length;
}
// Get the left position for the word selected
// Regex matches last non-whitespace character
let left = inputValue.slice(0, right).search(/\S+$/);
if (selectionStart === 0) {
left = 0;
} else if (selectionStart === inputValue.length && left < 0) {
left = inputValue.length;
} else if (left < 0) {
left = selectionStart;
}
return {
left,
right,
};
}
}
 
window.gl = window.gl || {};
Loading
Loading
Loading
Loading
@@ -57,28 +57,25 @@
 
static addWordToInput(tokenName, tokenValue = '') {
const input = document.querySelector('.filtered-search');
const inputValue = input.value;
const word = `${tokenName}:${tokenValue}`;
 
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value);
const lastSearchToken = searchToken.split(' ').last();
const lastInputCharacter = input.value[input.value.length - 1];
const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1];
// Remove the typed tokenName
if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') {
// Remove spaces after the colon
if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') {
input.value = input.value.trim();
}
input.value = input.value.slice(0, -1 * lastSearchToken.length);
} else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) {
// Remove the existing tokenValue
const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`;
input.value = input.value.slice(0, -1 * lastTokenString.length);
}
// Get the string to replace
const selectionStart = input.selectionStart;
const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
gl.FilteredSearchDropdownManager.updateInputCaretPosition(selectionStart, input);
}
static updateInputCaretPosition(selectionStart, input) {
// Reset the position
// Sometimes can end up at end of input
input.setSelectionRange(selectionStart, selectionStart);
const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
 
input.value += word;
input.setSelectionRange(right, right);
}
 
updateCurrentDropdownOffset() {
Loading
Loading
@@ -90,9 +87,10 @@
this.font = window.getComputedStyle(this.filteredSearchInput).font;
}
 
const input = this.filteredSearchInput;
const inputText = input.value.slice(0, input.selectionStart);
const filterIconPadding = 27;
const offset = gl.text
.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding;
const offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
 
this.mapping[key].reference.setOffset(offset);
}
Loading
Loading
@@ -148,9 +146,9 @@
 
setDropdown() {
const { lastToken, searchToken } = this.tokenizer
.processTokens(this.filteredSearchInput.value);
.processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
 
if (this.filteredSearchInput.value.split('').last() === ' ') {
if (this.currentDropdown) {
this.updateCurrentDropdownOffset();
}
 
Loading
Loading
Loading
Loading
@@ -30,11 +30,14 @@
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.tokenChange = this.tokenChange.bind(this);
 
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
}
 
Loading
Loading
@@ -43,6 +46,8 @@
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
}
 
Loading
Loading
@@ -188,6 +193,14 @@
}
return usernamesById;
}
tokenChange() {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const currentDropdownRef = dropdown.reference;
this.setDropdownWrapper();
currentDropdownRef.dispatchInputEvent();
}
}
 
window.gl = window.gl || {};
Loading
Loading
Loading
Loading
@@ -19,9 +19,12 @@ describe 'Filter issues', js: true, feature: true do
let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
let(:filtered_search) { find('.filtered-search') }
 
def input_filtered_search(search_term)
def input_filtered_search(search_term, submit: true)
filtered_search.set(search_term)
filtered_search.send_keys(:enter)
if submit
filtered_search.send_keys(:enter)
end
end
 
def expect_filtered_search_input(input)
Loading
Loading
@@ -43,6 +46,10 @@ describe 'Filter issues', js: true, feature: true do
end
end
 
def select_search_at_index(pos)
evaluate_script("el = document.querySelector('.filtered-search'); el.focus(); el.setSelectionRange(#{pos}, #{pos});")
end
before do
project.team << [user, :master]
project.team << [user2, :master]
Loading
Loading
@@ -522,6 +529,44 @@ describe 'Filter issues', js: true, feature: true do
end
end
 
describe 'overwrites selected filter' do
it 'changes author' do
input_filtered_search("author:@#{user.username}", submit: false)
select_search_at_index(3)
page.within '#js-dropdown-author' do
click_button user2.username
end
expect(filtered_search.value).to eq("author:@#{user2.username}")
end
it 'changes label' do
input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false)
select_search_at_index(27)
page.within '#js-dropdown-label' do
click_button label.name
end
expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name}")
end
it 'changes label correctly space is in previous label' do
input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false)
select_search_at_index(0)
page.within '#js-dropdown-label' do
click_button label.name
end
expect(filtered_search.value).to eq("label:~#{label.name}")
end
end
describe 'filter issues by text' do
context 'only text' do
it 'filters issues by searched text' do
Loading
Loading
Loading
Loading
@@ -31,41 +31,68 @@
});
 
describe('filterWithSymbol', () => {
let input;
const item = {
title: '@root',
};
 
beforeEach(() => {
setFixtures(`
<input type="text" id="test" />
`);
input = document.getElementById('test');
});
it('should filter without symbol', () => {
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo');
input.value = ':roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
});
 
it('should filter with symbol', () => {
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':@roo');
input.value = '@roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
});
 
it('should filter with colon', () => {
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':');
input.value = 'roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
});
});
 
describe('filterHint', () => {
let input;
beforeEach(() => {
setFixtures(`
<input type="text" id="test" />
`);
input = document.getElementById('test');
});
it('should filter', () => {
let updatedItem = gl.DropdownUtils.filterHint({
input.value = 'l';
let updatedItem = gl.DropdownUtils.filterHint(input, {
hint: 'label',
}, 'l');
});
expect(updatedItem.droplab_hidden).toBe(false);
 
updatedItem = gl.DropdownUtils.filterHint({
input.value = 'o';
updatedItem = gl.DropdownUtils.filterHint(input, {
hint: 'label',
}, 'o');
expect(updatedItem.droplab_hidden).toBe(true);
});
 
it('should return droplab_hidden false when item has no hint', () => {
const updatedItem = gl.DropdownUtils.filterHint({}, '');
const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
expect(updatedItem.droplab_hidden).toBe(false);
});
});
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment