summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Schatz <jschatz@gitlab.com>2017-03-07 17:55:12 +0000
committerJacob Schatz <jschatz@gitlab.com>2017-03-07 17:55:12 +0000
commit52d2e81acde268df96d454a75f7022ebbf7787c1 (patch)
tree9fbb6de958e841e3f85cc3b184c87a7abb9cfe12
parent1b04024caedd07fff53a76441bc9947150e9fb6c (diff)
parentce9236c004d4daeb4389f4d8e2584e742846eb8f (diff)
downloadgitlab-ce-52d2e81acde268df96d454a75f7022ebbf7787c1.tar.gz
Merge branch 'double-click-tokens-editable' into 'filtered-search-visual-tokens'
Double click tokens editable See merge request !9628
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es66
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es66
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es643
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6122
-rw-r--r--app/assets/stylesheets/framework/filters.scss2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml3
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb306
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es650
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js.es677
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6443
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js.es638
13 files changed, 926 insertions, 178 deletions
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index 5cdf11c6a2c..f61be741b4a 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -37,11 +37,14 @@ require('../window')(function(w){
}
}
- self.hook.list[config.method].call(self.hook.list, data);
+ if (!self.destroyed) {
+ self.hook.list[config.method].call(self.hook.list, data);
+ }
},
init: function init(hook) {
var self = this;
+ self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.droplabAjax;
this.hook = hook;
@@ -79,6 +82,7 @@ require('../window')(function(w){
destroy: function() {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ this.destroyed = true;
if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
index b666cac5bf0..14f0d0d0ff2 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
@@ -72,7 +72,7 @@
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
- gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
+ gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
}
// Return boolean based on whether it was set
@@ -100,8 +100,8 @@
}
});
- const inputValue = document.querySelector('.filtered-search').value;
- values.push(inputValue);
+ const input = document.querySelector('.filtered-search');
+ values.push(input && input.value);
return values.join(' ');
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
index dd565da507e..134bdc6ad80 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -35,7 +35,7 @@
if (!dataValueSet) {
const value = getValueFunction(selected);
- gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
+ gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.dismissDropdown();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
index be4c610f6dd..c3247f52da8 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -58,12 +58,16 @@
};
}
- static addWordToInput(tokenName, tokenValue = '') {
+ static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = document.querySelector('.filtered-search');
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
input.value = '';
+ if (clicked) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ }
+
// Get the string to replace
// let newCaretPosition = input.selectionStart;
// const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index 935daf3199b..9e6692e3d21 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -34,6 +34,7 @@
this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
+ this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.tokenChange = this.tokenChange.bind(this);
this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
@@ -46,8 +47,10 @@
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.addEventListener('dblclick', FilteredSearchManager.editToken);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
}
@@ -62,8 +65,10 @@
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.removeEventListener('dblclick', FilteredSearchManager.editToken);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
}
@@ -71,7 +76,7 @@
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
- const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualToken();
+ const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (this.filteredSearchInput.value === '' && lastVisualToken) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
@@ -117,6 +122,26 @@
}
}
+ unselectEditTokens(e) {
+ const inputContainer = document.querySelector('.filtered-search-input-container');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementScrollContainer = e.target.classList.contains('scroll-container');
+
+ if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementScrollContainer) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ this.dropdownManager.resetDropdowns();
+ }
+ }
+
+ static editToken(e) {
+ const token = e.target.closest('.js-visual-token');
+
+ if (token) {
+ gl.FilteredSearchVisualTokens.editToken(token);
+ }
+ }
+
toggleClearSearchButton() {
const query = gl.DropdownUtils.getSearchQuery();
const hidden = 'hidden';
@@ -155,7 +180,19 @@
e.preventDefault();
this.filteredSearchInput.value = '';
- this.filteredSearchInput.parentElement.querySelector('.tokens-container').innerHTML = '';
+
+ const removeElements = [];
+
+ [].forEach.call(this.tokensContainer.children, (t) => {
+ if (t.classList.contains('js-visual-token')) {
+ removeElements.push(t);
+ }
+ });
+
+ removeElements.forEach((el) => {
+ el.parentElement.removeChild(el);
+ });
+
this.clearSearchButton.classList.add('hidden');
this.handleInputPlaceholder();
@@ -167,7 +204,7 @@
const { tokens, searchToken }
= gl.FilteredSearchTokenizer.processTokens(input.value);
const { isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualToken();
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (isLastVisualTokenValid) {
tokens.forEach((t) => {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6
index 4f3efea8e16..78b245726ee 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js.es6
@@ -1,11 +1,11 @@
class FilteredSearchVisualTokens {
- static getLastVisualToken() {
- const tokensContainer = document.querySelector('.tokens-container');
- const visualTokens = tokensContainer.children;
- const lastVisualToken = visualTokens[visualTokens.length - 1];
+ static getLastVisualTokenBeforeInput() {
+ const inputLi = document.querySelector('.input-token');
+ const lastVisualToken = inputLi && inputLi.previousElementSibling;
+
return {
lastVisualToken,
- isLastVisualTokenValid: visualTokens.length === 0 || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null),
+ isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null),
};
}
@@ -32,18 +32,22 @@ class FilteredSearchVisualTokens {
}
}
+ static createVisualTokenElementHTML() {
+ return `
+ <div class="selectable" role="button">
+ <div class="name"></div>
+ <div class="value"></div>
+ </div>
+ `;
+ }
+
static addVisualTokenElement(name, value, isSearchTerm) {
const li = document.createElement('li');
li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
if (value) {
- li.innerHTML = `
- <div class="selectable" role="button">
- <div class="name"></div>
- <div class="value"></div>
- </div>
- `;
+ li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
li.querySelector('.value').innerText = value;
} else {
li.innerHTML = '<div class="name"></div>';
@@ -51,12 +55,25 @@ class FilteredSearchVisualTokens {
li.querySelector('.name').innerText = name;
const tokensContainer = document.querySelector('.tokens-container');
- tokensContainer.appendChild(li);
+ const input = document.querySelector('.filtered-search');
+ tokensContainer.insertBefore(li, input.parentElement);
+ }
+
+ static addValueToPreviousVisualTokenElement(value) {
+ const { lastVisualToken, isLastVisualTokenValid } =
+ FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
+ const name = FilteredSearchVisualTokens.getLastTokenPartial();
+ lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+ lastVisualToken.querySelector('.name').innerText = name;
+ lastVisualToken.querySelector('.value').innerText = value;
+ }
}
static addFilterVisualToken(tokenName, tokenValue) {
const { lastVisualToken, isLastVisualTokenValid }
- = FilteredSearchVisualTokens.getLastVisualToken();
+ = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
if (isLastVisualTokenValid) {
@@ -72,11 +89,17 @@ class FilteredSearchVisualTokens {
}
static addSearchVisualToken(searchTerm) {
- FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
+ lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
+ } else {
+ FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
+ }
}
static getLastTokenPartial() {
- const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualToken();
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (!lastVisualToken) return '';
@@ -90,7 +113,7 @@ class FilteredSearchVisualTokens {
}
static removeLastTokenPartial() {
- const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualToken();
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
if (lastVisualToken) {
const value = lastVisualToken.querySelector('.value');
@@ -104,6 +127,73 @@ class FilteredSearchVisualTokens {
}
}
}
+
+ static tokenizeInput() {
+ const input = document.querySelector('.filtered-search');
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (input.value) {
+ if (isLastVisualTokenValid) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value);
+ } else {
+ FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value);
+ }
+
+ input.value = '';
+ }
+ }
+
+ static editToken(token) {
+ const input = document.querySelector('.filtered-search');
+
+ FilteredSearchVisualTokens.tokenizeInput();
+
+ // Replace token with input field
+ const tokenContainer = token.parentElement;
+ const inputLi = input.parentElement;
+ tokenContainer.replaceChild(inputLi, token);
+
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+
+ if (token.classList.contains('filtered-search-token')) {
+ FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
+ input.value = value.innerText;
+ } else {
+ // token is a search term
+ input.value = name.innerText;
+ }
+
+ // Opens dropdown
+ const inputEvent = new Event('input');
+ input.dispatchEvent(inputEvent);
+
+ // Adds cursor to input
+ input.focus();
+ }
+
+ static moveInputToTheRight() {
+ const input = document.querySelector('.filtered-search');
+ const inputLi = input.parentElement;
+ const tokenContainer = document.querySelector('.tokens-container');
+
+ if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
+ FilteredSearchVisualTokens.tokenizeInput();
+
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!isLastVisualTokenValid) {
+ const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
+ }
+
+ tokenContainer.removeChild(inputLi);
+ tokenContainer.appendChild(inputLi);
+ }
+ }
}
window.gl = window.gl || {};
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 64e3251a849..d418d172434 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -130,10 +130,8 @@
}
.form-control {
- left: 30px;
position: relative;
min-width: 200px;
- max-width: calc(100% - 30px);
padding-left: 0;
padding-right: 25px;
border-color: transparent;
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 95c9bc7229e..fefe4802fc1 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -13,7 +13,8 @@
.filtered-search-input-container
.scroll-container
%ul.tokens-container.list-unstyled
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
+ %li.input-token
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
new file mode 100644
index 00000000000..b62a6d7913d
--- /dev/null
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -0,0 +1,306 @@
+require 'rails_helper'
+
+describe 'Visual tokens', js: true, feature: true do
+ include FilteredSearchHelpers
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user, name: 'administrator', username: 'root') }
+ let!(:user_rock) { create(:user, name: 'The Rock', username: 'rock') }
+ let!(:milestone_nine) { create(:milestone, title: '9.0', project: project) }
+ let!(:milestone_ten) { create(:milestone, title: '10.0', project: project) }
+ let!(:label) { create(:label, project: project, title: 'abc') }
+ let!(:cc_label) { create(:label, project: project, title: 'Community Contribution') }
+
+ let(:filtered_search) { find('.filtered-search') }
+ let(:filter_author_dropdown) { find("#js-dropdown-author .filter-dropdown") }
+ let(:filter_assignee_dropdown) { find("#js-dropdown-assignee .filter-dropdown") }
+ let(:filter_milestone_dropdown) { find("#js-dropdown-milestone .filter-dropdown") }
+ let(:filter_label_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+ def is_input_focused
+ page.evaluate_script("document.activeElement.classList.contains('filtered-search')")
+ end
+
+ before do
+ project.add_user(user, :master)
+ project.add_user(user_rock, :master)
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'editing author token' do
+ before do
+ input_filtered_search('author:@root assignee:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ end
+
+ it 'opens author dropdown' do
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('@root')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-author .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-author', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-author', visible: false)
+ end
+
+ describe 'selecting different author from dropdown' do
+ before do
+ filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click
+ end
+
+ it 'changes value in visual token' do
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}")
+ end
+
+ it 'moves input to the right' do
+ expect(is_input_focused).to eq(true)
+ end
+ end
+ end
+
+ describe 'editing assignee token' do
+ before do
+ input_filtered_search('assignee:@root author:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ end
+
+ it 'opens assignee dropdown' do
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('@root')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-assignee', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-assignee', visible: false)
+ end
+
+ describe 'selecting static option from dropdown' do
+ before do
+ find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click
+ end
+
+ it 'changes value in visual token' do
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq('none')
+ end
+
+ it 'moves input to the right' do
+ expect(is_input_focused).to eq(true)
+ end
+ end
+ end
+
+ describe 'editing milestone token' do
+ before do
+ input_filtered_search('milestone:%10.0 author:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
+ end
+
+ it 'opens milestone dropdown' do
+ expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_ten.title)).to be_visible
+ expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_nine.title)).to be_visible
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ end
+
+ it 'selects static option from dropdown' do
+ find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click
+
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming')
+ expect(is_input_focused).to eq(true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('%10.0')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-milestone', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-milestone', visible: false)
+ end
+ end
+
+ describe 'editing label token' do
+ before do
+ input_filtered_search("label:~#{label.title} author:none", submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ first('#js-dropdown-label .filter-dropdown .filter-dropdown-item')
+ end
+
+ it 'opens label dropdown' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ end
+
+ it 'selects option from dropdown' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+
+ find("#js-dropdown-label").find('.filter-dropdown-item', text: cc_label.title).click
+
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("~\"#{cc_label.title}\"")
+ expect(is_input_focused).to eq(true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input("~#{label.title}")
+ end
+
+ it 'filters value' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+
+ filtered_search.send_keys(:backspace)
+
+ filter_label_dropdown.find('.filter-dropdown-item')
+
+ expect(page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size).to eq(1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-label', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-label', visible: false)
+ end
+ end
+
+ describe 'add new token after editing existing token' do
+ before do
+ input_filtered_search('author:@root assignee:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ filtered_search.send_keys(' ')
+ end
+
+ describe 'opens dropdowns' do
+ it 'opens hint dropdown' do
+ expect(page).to have_css('#js-dropdown-hint', visible: true)
+ end
+
+ it 'opens author dropdown' do
+ filtered_search.send_keys('author:')
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+
+ it 'opens assignee dropdown' do
+ filtered_search.send_keys('assignee:')
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ end
+
+ it 'opens milestone dropdown' do
+ filtered_search.send_keys('milestone:')
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ end
+
+ it 'opens label dropdown' do
+ filtered_search.send_keys('label:')
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ end
+ end
+
+ describe 'creates visual tokens' do
+ it 'creates author token' do
+ filtered_search.send_keys('author:@thomas ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Author')
+ expect(token.find('.value').text).to eq('@thomas')
+ end
+
+ it 'creates assignee token' do
+ filtered_search.send_keys('assignee:@thomas ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Assignee')
+ expect(token.find('.value').text).to eq('@thomas')
+ end
+
+ it 'creates milestone token' do
+ filtered_search.send_keys('milestone:none ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Milestone')
+ expect(token.find('.value').text).to eq('none')
+ end
+
+ it 'creates label token' do
+ filtered_search.send_keys('label:~Backend ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Label')
+ expect(token.find('.value').text).to eq('~Backend')
+ end
+ end
+
+ it 'does not tokenize incomplete token' do
+ filtered_search.send_keys('author:')
+
+ find('#content-body').click
+ token = page.all('.tokens-container .js-visual-token')[1]
+
+ expect_filtered_search_input_empty
+ expect(token.find('.name').text).to eq('Author')
+ end
+ end
+end
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
index 7ddc22380a5..a1da3396d7b 100644
--- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
@@ -15,31 +15,21 @@ require('~/filtered_search/filtered_search_dropdown_manager');
}
beforeEach(() => {
- // spyOn(gl.FilteredSearchVisualTokens, 'addVisualToken').and.callFake(() => {});
- const div = document.createElement('div');
- // div.classList.add('spec-container');
-
- div.innerHTML = `
- <ul class="tokens-container"></ul>
- <input class="filtered-search">
- `;
- document.body.appendChild(div);
- });
-
- afterEach(() => {
- document.querySelector('.filtered-search').outerHTML = '';
- document.querySelector('.tokens-container').innerHTML = '';
+ setFixtures(`
+ <ul class="tokens-container">
+ <li class="input-token">
+ <input class="filtered-search">
+ </li>
+ </ul>
+ `);
});
describe('input has no existing value', () => {
it('should add just tokenName', () => {
gl.FilteredSearchDropdownManager.addWordToInput('milestone');
- const tokensContainer = document.querySelector('.tokens-container');
- const token = tokensContainer.children[0];
+ const token = document.querySelector('.tokens-container .js-visual-token');
- expect(tokensContainer.children.length).toBe(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('milestone');
expect(getInputValue()).toBe('');
@@ -48,11 +38,8 @@ require('~/filtered_search/filtered_search_dropdown_manager');
it('should add tokenName and tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label');
- const tokensContainer = document.querySelector('.tokens-container');
- let token = tokensContainer.children[0];
+ let token = document.querySelector('.tokens-container .js-visual-token');
- expect(tokensContainer.children.length).toBe(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label');
expect(getInputValue()).toBe('');
@@ -60,10 +47,8 @@ require('~/filtered_search/filtered_search_dropdown_manager');
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
// We have to get that reference again
// Because gl.FilteredSearchDropdownManager deletes the previous token
- token = tokensContainer.children[0];
+ token = document.querySelector('.tokens-container .js-visual-token');
- expect(tokensContainer.children.length).toBe(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.value').innerText).toBe('none');
@@ -76,11 +61,8 @@ require('~/filtered_search/filtered_search_dropdown_manager');
setInputValue('a');
gl.FilteredSearchDropdownManager.addWordToInput('author');
- const tokensContainer = document.querySelector('.tokens-container');
- const token = tokensContainer.children[0];
+ const token = document.querySelector('.tokens-container .js-visual-token');
- expect(tokensContainer.children.length).toBe(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author');
expect(getInputValue()).toBe('');
@@ -92,11 +74,8 @@ require('~/filtered_search/filtered_search_dropdown_manager');
setInputValue('roo');
gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
- const tokensContainer = document.querySelector('.tokens-container');
- const token = tokensContainer.children[0];
+ const token = document.querySelector('.tokens-container .js-visual-token');
- expect(tokensContainer.children.length).toBe(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('author');
expect(token.querySelector('.value').innerText).toBe('@root');
@@ -109,11 +88,8 @@ require('~/filtered_search/filtered_search_dropdown_manager');
setInputValue('"test ');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
- const tokensContainer = document.querySelector('.tokens-container');
- const token = tokensContainer.children[0];
+ const token = document.querySelector('.tokens-container .js-visual-token');
- expect(tokensContainer.children.length).toBe(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toBe('label');
expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
index 8312928a8c5..81c1d81d181 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
@@ -29,13 +29,16 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
beforeEach(() => {
setFixtures(`
- <form>
- <ul class="tokens-container list-unstyled"></ul>
- <input type='text' class='filtered-search' placeholder='${placeholder}' />
- <button class="clear-search" type="button">
- <i class="fa fa-times"></i>
- </button>
- </form>
+ <div class="filtered-search-input-container">
+ <form>
+ <ul class="tokens-container list-unstyled">
+ ${FilteredSearchSpecHelper.createInputHTML(placeholder)}
+ </ul>
+ <button class="clear-search" type="button">
+ <i class="fa fa-times"></i>
+ </button>
+ </form>
+ </div>
`);
spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
@@ -44,44 +47,44 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
+ spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
manager = new gl.FilteredSearchManager();
});
- afterEach(() => {
- tokensContainer.innerHTML = '';
- });
-
describe('search', () => {
const defaultParams = '?scope=all&utf8=✓&state=opened';
- it('should search with a single word', () => {
+ it('should search with a single word', (done) => {
input.value = 'searchTerm';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ done();
});
manager.search();
});
- it('should search with multiple words', () => {
+ it('should search with multiple words', (done) => {
input.value = 'awesome search terms';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
+ done();
});
manager.search();
});
- it('should search with special characters', () => {
+ it('should search with special characters', (done) => {
input.value = '~!@#$%^&*()_+{}:<>,.?/';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
+ done();
});
manager.search();
@@ -103,7 +106,9 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
it('should not render placeholder when there are tokens and no input', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug');
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
const event = new Event('input');
input.dispatchEvent(event);
@@ -115,7 +120,9 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
describe('checkForBackspace', () => {
describe('tokens and no input', () => {
beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug');
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
});
it('removes last token', () => {
@@ -148,38 +155,42 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
describe('removeSelectedToken', () => {
+ function getVisualTokens() {
+ return tokensContainer.querySelectorAll('.js-visual-token');
+ }
+
beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true);
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
});
it('removes selected token when the backspace key is pressed', () => {
- expect(tokensContainer.children.length).toEqual(1);
+ expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown');
- expect(tokensContainer.children.length).toEqual(0);
+ expect(getVisualTokens().length).toEqual(0);
});
it('removes selected token when the delete key is pressed', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true);
-
- expect(tokensContainer.children.length).toEqual(1);
+ expect(getVisualTokens().length).toEqual(1);
dispatchDeleteEvent(document, 'keydown');
- expect(tokensContainer.children.length).toEqual(0);
+ expect(getVisualTokens().length).toEqual(0);
});
it('updates the input placeholder after removal', () => {
manager.handleInputPlaceholder();
expect(input.placeholder).toEqual('');
- expect(tokensContainer.children.length).toEqual(1);
+ expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown');
expect(input.placeholder).not.toEqual('');
- expect(tokensContainer.children.length).toEqual(0);
+ expect(getVisualTokens().length).toEqual(0);
});
it('updates the clear button after removal', () => {
@@ -188,31 +199,35 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
const clearButton = document.querySelector('.clear-search');
expect(clearButton.classList.contains('hidden')).toEqual(false);
- expect(tokensContainer.children.length).toEqual(1);
+ expect(getVisualTokens().length).toEqual(1);
dispatchBackspaceEvent(document, 'keydown');
expect(clearButton.classList.contains('hidden')).toEqual(true);
- expect(tokensContainer.children.length).toEqual(0);
+ expect(getVisualTokens().length).toEqual(0);
});
});
describe('unselects token', () => {
beforeEach(() => {
- tokensContainer.innerHTML = `
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
- `;
+ `);
});
it('unselects token when input is clicked', () => {
const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
- input.click();
+ // Click directly on input attached to document
+ // so that the click event will propagate properly
+ document.querySelector('.filtered-search').click();
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
expect(selectedToken.classList.contains('selected')).toEqual(false);
});
@@ -220,10 +235,12 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
document.body.click();
expect(selectedToken.classList.contains('selected')).toEqual(false);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
});
});
});
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6
index 4d6eda83e79..5a8617395c1 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js.es6
@@ -7,72 +7,106 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
beforeEach(() => {
setFixtures(`
- <ul class="tokens-container"></ul>
+ <ul class="tokens-container">
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ </ul>
`);
tokensContainer = document.querySelector('.tokens-container');
});
- afterEach(() => {
- tokensContainer.innerHTML = '';
- });
-
- describe('getLastVisualToken', () => {
+ describe('getLastVisualTokenBeforeInput', () => {
it('returns when there are no visual tokens', () => {
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualToken();
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- expect(lastVisualToken).toEqual(undefined);
+ expect(lastVisualToken).toEqual(null);
expect(isLastVisualTokenValid).toEqual(true);
});
- it('returns when there is one visual token', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug');
+ describe('input is the last item in tokensContainer', () => {
+ it('returns when there is one visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
- const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualToken();
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
- expect(isLastVisualTokenValid).toEqual(true);
- });
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
- it('returns when there is an incomplete visual token', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author');
+ it('returns when there is an incomplete visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author'),
+ );
- const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualToken();
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
- expect(isLastVisualTokenValid).toEqual(false);
- });
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
- it('returns when there are multiple visual tokens', () => {
- tokensContainer.innerHTML = `
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
- ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
- `;
+ it('returns when there are multiple visual tokens', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
- const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualToken();
- const items = document.querySelectorAll('.tokens-container li');
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const items = document.querySelectorAll('.tokens-container .js-visual-token');
- expect(lastVisualToken).toEqual(items[items.length - 1]);
- expect(isLastVisualTokenValid).toEqual(true);
+ expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ it('returns when there are multiple visual tokens and an incomplete visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const items = document.querySelectorAll('.tokens-container .js-visual-token');
+
+ expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
});
- it('returns when there are multiple visual tokens and an incomplete visual token', () => {
- tokensContainer.innerHTML = `
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
- ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
- ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
- `;
+ describe('input is a middle item in tokensContainer', () => {
+ it('returns last token before input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
- const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualToken();
- const items = document.querySelectorAll('.tokens-container li');
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
- expect(lastVisualToken).toEqual(items[items.length - 1]);
- expect(isLastVisualTokenValid).toEqual(false);
+ it('returns last partial token before input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
});
});
@@ -85,10 +119,10 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
it('removes the selected class from buttons', () => {
- tokensContainer.innerHTML = `
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@author')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '%123', true)}
- `;
+ `);
const selected = tokensContainer.querySelector('.js-visual-token .selected');
expect(selected.classList.contains('selected')).toEqual(true);
@@ -101,11 +135,11 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
describe('selectToken', () => {
beforeEach(() => {
- tokensContainer.innerHTML = `
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
- `;
+ `);
});
it('removes the selected class if it has selected class', () => {
@@ -140,7 +174,9 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
describe('removeSelectedToken', () => {
it('does not remove when there are no selected tokens', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none');
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
+ );
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
@@ -150,7 +186,9 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
it('removes selected token', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true);
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
@@ -160,13 +198,41 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
});
+ describe('createVisualTokenElementHTML', () => {
+ let tokenElement;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="test-area">
+ ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()}
+ </div>
+ `);
+
+ tokenElement = document.querySelector('.test-area').firstElementChild;
+ });
+
+ it('contains name div', () => {
+ expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
+ });
+
+ it('contains value div', () => {
+ expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
+ });
+
+ it('contains selectable class', () => {
+ expect(tokenElement.classList.contains('selectable')).toEqual(true);
+ });
+
+ it('contains button role', () => {
+ expect(tokenElement.getAttribute('role')).toEqual('button');
+ });
+ });
+
describe('addVisualTokenElement', () => {
it('renders search visual tokens', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true);
- const token = tokensContainer.children[0];
+ const token = tokensContainer.querySelector('.js-visual-token');
- expect(tokensContainer.children.length).toEqual(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-term')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('search term');
expect(token.querySelector('.value')).toEqual(null);
@@ -174,10 +240,8 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
it('renders filter visual token name', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone');
- const token = tokensContainer.children[0];
+ const token = tokensContainer.querySelector('.js-visual-token');
- expect(tokensContainer.children.length).toEqual(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('milestone');
expect(token.querySelector('.value')).toEqual(null);
@@ -185,23 +249,75 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
it('renders filter visual token name and value', () => {
gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
- const token = tokensContainer.children[0];
+ const token = tokensContainer.querySelector('.js-visual-token');
- expect(tokensContainer.children.length).toEqual(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('label');
expect(token.querySelector('.value').innerText).toEqual('Frontend');
});
+
+ it('inserts visual token before input', () => {
+ tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root'));
+
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ const tokens = tokensContainer.querySelectorAll('.js-visual-token');
+ const labelToken = tokens[0];
+ const assigneeToken = tokens[1];
+
+ expect(labelToken.classList.contains('filtered-search-token')).toEqual(true);
+ expect(labelToken.querySelector('.name').innerText).toEqual('label');
+ expect(labelToken.querySelector('.value').innerText).toEqual('Frontend');
+
+ expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true);
+ expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee');
+ expect(assigneeToken.querySelector('.value').innerText).toEqual('@root');
+ });
+ });
+
+ describe('addValueToPreviousVisualTokenElement', () => {
+ it('does not add when previous visual token element has no value', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root'),
+ );
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+
+ expect(original).toEqual(tokensContainer.innerHTML);
+ });
+
+ it('does not add when previous visual token element is a search', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ `);
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+
+ expect(original).toEqual(tokensContainer.innerHTML);
+ });
+
+ it('adds value to previous visual filter token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label'),
+ );
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ const updatedToken = tokensContainer.querySelector('.js-visual-token');
+
+ expect(updatedToken.querySelector('.name').innerText).toEqual('label');
+ expect(updatedToken.querySelector('.value').innerText).toEqual('value');
+ expect(original).not.toEqual(tokensContainer.innerHTML);
+ });
});
describe('addFilterVisualToken', () => {
it('creates visual token with just tokenName', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
- const token = tokensContainer.children[0];
+ const token = tokensContainer.querySelector('.js-visual-token');
- expect(tokensContainer.children.length).toEqual(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('milestone');
expect(token.querySelector('.value')).toEqual(null);
@@ -210,10 +326,8 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
it('creates visual token with just tokenValue', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17');
- const token = tokensContainer.children[0];
+ const token = tokensContainer.querySelector('.js-visual-token');
- expect(tokensContainer.children.length).toEqual(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('milestone');
expect(token.querySelector('.value').innerText).toEqual('%8.17');
@@ -221,10 +335,8 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
it('creates full visual token', () => {
gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john');
- const token = tokensContainer.children[0];
+ const token = tokensContainer.querySelector('.js-visual-token');
- expect(tokensContainer.children.length).toEqual(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('assignee');
expect(token.querySelector('.value').innerText).toEqual('@john');
@@ -234,27 +346,42 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
describe('addSearchVisualToken', () => {
it('creates search visual token', () => {
gl.FilteredSearchVisualTokens.addSearchVisualToken('search term');
- const token = tokensContainer.children[0];
+ const token = tokensContainer.querySelector('.js-visual-token');
- expect(tokensContainer.children.length).toEqual(1);
- expect(token.classList.contains('js-visual-token')).toEqual(true);
expect(token.classList.contains('filtered-search-term')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('search term');
expect(token.querySelector('.value')).toEqual(null);
});
+
+ it('appends to previous search visual token if previous token was a search token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ `);
+
+ gl.FilteredSearchVisualTokens.addSearchVisualToken('append this');
+ const token = tokensContainer.querySelector('.filtered-search-term');
+
+ expect(token.querySelector('.name').innerText).toEqual('search term append this');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
});
describe('getLastTokenPartial', () => {
it('should get last token value', () => {
const value = '~bug';
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value);
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value),
+ );
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value);
});
it('should get last token name if there is no value', () => {
const name = 'assignee';
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name);
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name),
+ );
expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name);
});
@@ -266,7 +393,9 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
describe('removeLastTokenPartial', () => {
it('should remove the last token value if it exists', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"');
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"'),
+ );
expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null);
@@ -276,7 +405,9 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
it('should remove the last token name if there is no value', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone');
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone'),
+ );
expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null);
@@ -292,5 +423,167 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
expect(tokensContainer.innerHTML).toEqual(html);
});
});
+
+ describe('tokenizeInput', () => {
+ it('does not do anything if there is no input', () => {
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ expect(tokensContainer.innerHTML).toEqual(original);
+ });
+
+ it('adds search visual token if previous visual token is valid', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('assignee', 'none'),
+ );
+
+ const input = document.querySelector('.filtered-search');
+ input.value = 'some value';
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ const newToken = tokensContainer.querySelector('.filtered-search-term');
+
+ expect(input.value).toEqual('');
+ expect(newToken.querySelector('.name').innerText).toEqual('some value');
+ expect(newToken.querySelector('.value')).toEqual(null);
+ });
+
+ it('adds value to previous visual token element if previous visual token is invalid', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee'),
+ );
+
+ const input = document.querySelector('.filtered-search');
+ input.value = '@john';
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ const updatedToken = tokensContainer.querySelector('.filtered-search-token');
+
+ expect(input.value).toEqual('');
+ expect(updatedToken.querySelector('.name').innerText).toEqual('assignee');
+ expect(updatedToken.querySelector('.value').innerText).toEqual('@john');
+ });
+ });
+
+ describe('editToken', () => {
+ let input;
+ let token;
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')}
+ `);
+
+ input = document.querySelector('.filtered-search');
+ token = document.querySelector('.js-visual-token');
+ });
+
+ it('tokenize\'s existing input', () => {
+ input.value = 'some text';
+ spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+ expect(input.value).not.toEqual('some text');
+ });
+
+ it('moves input to the token position', () => {
+ expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null);
+ expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null);
+ });
+
+ it('input contains the visual token value', () => {
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(input.value).toEqual('none');
+ });
+
+ describe('selected token is a search term token', () => {
+ beforeEach(() => {
+ token = document.querySelector('.filtered-search-term');
+ });
+
+ it('token is removed', () => {
+ expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null);
+ });
+
+ it('input has the same value as removed token', () => {
+ expect(input.value).toEqual('');
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(input.value).toEqual('search');
+ });
+ });
+ });
+
+ describe('moveInputTotheRight', () => {
+ it('does nothing if the input is already the right most element', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'),
+ );
+
+ spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough();
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+ expect(gl.FilteredSearchVisualTokens.tokenizeInput).not.toHaveBeenCalled();
+ expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
+ });
+
+ it('tokenize\'s input', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ document.querySelector('.filtered-search').value = 'none';
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ const value = tokensContainer.querySelector('.js-visual-token .value');
+
+ expect(value.innerText).toEqual('none');
+ });
+
+ it('converts input into search term token if last token is valid', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ document.querySelector('.filtered-search').value = 'test';
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ const searchValue = tokensContainer.querySelector('.filtered-search-term .name');
+
+ expect(searchValue.innerText).toEqual('test');
+ });
+
+ it('moves the input to the right most element', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+ expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null);
+ });
+ });
});
})();
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js.es6 b/spec/javascripts/helpers/filtered_search_spec_helper.js.es6
index 31f4d93f8b8..c891518fce9 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js.es6
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js.es6
@@ -1,13 +1,20 @@
class FilteredSearchSpecHelper {
- static createFilterVisualTokenHTML(name, value, isSelected = false) {
- return `
- <li class="js-visual-token filtered-search-token">
- <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
- <div class="name">${name}</div>
- <div class="value">${value}</div>
- </div>
- </li>
+ static createFilterVisualTokenHTML(name, value, isSelected) {
+ return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML;
+ }
+
+ static createFilterVisualToken(name, value, isSelected = false) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token', 'filtered-search-token');
+
+ li.innerHTML = `
+ <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
+ <div class="name">${name}</div>
+ <div class="value">${value}</div>
+ </div>
`;
+
+ return li;
}
static createNameFilterVisualTokenHTML(name) {
@@ -25,6 +32,21 @@ class FilteredSearchSpecHelper {
</li>
`;
}
+
+ static createInputHTML(placeholder = '') {
+ return `
+ <li class="input-token">
+ <input type='text' class='filtered-search' placeholder='${placeholder}' />
+ </li>
+ `;
+ }
+
+ static createTokensContainerHTML(html, inputPlaceholder) {
+ return `
+ ${html}
+ ${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)}
+ `;
+ }
}
module.exports = FilteredSearchSpecHelper;