diff options
author | Clement Ho <ClemMakesApps@gmail.com> | 2017-02-24 12:21:53 -0600 |
---|---|---|
committer | Clement Ho <ClemMakesApps@gmail.com> | 2017-02-24 20:21:12 -0600 |
commit | e7926605e2225f5a956954e134282118b1ed4f9e (patch) | |
tree | cf7a724812f75d115b5cc30e60519642bc1ec561 | |
parent | 4e4e7f20811bac3f9989a59a75c5754c79a27593 (diff) | |
download | gitlab-ce-backspace-to-edit-visual-token.tar.gz |
Add ability to edit visual token using backspacebackspace-to-edit-visual-token
5 files changed, 200 insertions, 38 deletions
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 c1fc1111ecf..081c2fa9aeb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -63,6 +63,13 @@ // 8 = Backspace Key // 46 = Delete Key if (e.keyCode === 8 || e.keyCode === 46) { + const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualToken(); + + if (this.filteredSearchInput.value === '' && lastVisualToken) { + this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + } + // Reposition dropdown so that it is aligned with cursor this.dropdownManager.updateCurrentDropdownOffset(); } 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 124d1740767..33f9d2e2651 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 @@ -45,6 +45,34 @@ class FilteredSearchVisualTokens { static addSearchVisualToken(searchTerm) { FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true); } + + static getLastTokenPartial() { + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualToken(); + + if (!lastVisualToken) return ''; + + const value = lastVisualToken.querySelector('.value'); + const name = lastVisualToken.querySelector('.name'); + + const valueText = value ? value.innerText : ''; + const nameText = name ? name.innerText : ''; + + return valueText || nameText; + } + + static removeLastTokenPartial() { + const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualToken(); + + if (lastVisualToken) { + const value = lastVisualToken.querySelector('.value'); + + if (value) { + lastVisualToken.removeChild(value); + } else { + lastVisualToken.parentElement.removeChild(lastVisualToken); + } + } + } } window.gl = window.gl || {}; 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 2440e55bb47..975fb1da40a 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 @@ -4,6 +4,7 @@ require('~/filtered_search/filtered_search_token_keys'); require('~/filtered_search/filtered_search_tokenizer'); require('~/filtered_search/filtered_search_dropdown_manager'); require('~/filtered_search/filtered_search_manager'); +const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); (() => { describe('Filtered Search Manager', () => { @@ -107,12 +108,7 @@ require('~/filtered_search/filtered_search_manager'); }); it('should not render placeholder when there are tokens and no input', () => { - tokensContainer.innerHTML = ` - <li class="js-visual-token filtered-search-token"> - <div class="name">label</div> - <div class="value">~bug</div> - </li> - `; + tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'); const event = new Event('input'); input.dispatchEvent(event); @@ -120,5 +116,82 @@ require('~/filtered_search/filtered_search_manager'); expect(input.placeholder).toEqual(''); }); }); + + describe('checkForBackspace', () => { + let tokensContainer; + const backspaceKey = 8; + const deleteKey = 46; + + beforeEach(() => { + setFixtures(` + <form> + <ul class="tokens-container list-unstyled"></ul> + <input type='text' class='filtered-search' /> + <button class="clear-search" type="button"> + <i class="fa fa-times"></i> + </button> + </form> + `); + + spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); + spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); + + spyOn(gl.utils, 'getParameterByName').and.returnValue(null); + + input = document.querySelector('.filtered-search'); + tokensContainer = document.querySelector('.tokens-container'); + return new gl.FilteredSearchManager(); + }); + + afterEach(() => { + input.outerHTML = ''; + tokensContainer.innerHTML = ''; + }); + + describe('tokens and no input', () => { + beforeEach(() => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'); + }); + + it('removes last token', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + + const event = new Event('keyup'); + event.keyCode = backspaceKey; + input.dispatchEvent(event); + + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); + }); + + it('sets the input', () => { + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + + const event = new Event('keyup'); + event.keyCode = deleteKey; + input.dispatchEvent(event); + + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); + expect(input.value).toEqual('~bug'); + }); + }); + + it('does not remove token or change input when there is existing input', () => { + spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + + input.value = 'text'; + + const event = new Event('keyup'); + event.keyCode = deleteKey; + input.dispatchEvent(event); + + expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(input.value).toEqual('text'); + }); + }); }); })(); 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 4f07dfbd7d9..8c5150a6134 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 @@ -1,4 +1,5 @@ require('~/filtered_search/filtered_search_visual_tokens'); +const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); (() => { describe('Filtered Search Visual Tokens', () => { @@ -25,12 +26,7 @@ require('~/filtered_search/filtered_search_visual_tokens'); }); it('returns when there is one visual token', () => { - tokensContainer.innerHTML = ` - <li class="js-visual-token filtered-search-token"> - <div class="name">label</div> - <div class="value">~bug</div> - </li> - `; + tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'); const { lastVisualToken, isLastVisualTokenValid } = gl.FilteredSearchVisualTokens.getLastVisualToken(); @@ -40,11 +36,7 @@ require('~/filtered_search/filtered_search_visual_tokens'); }); it('returns when there is an incomplete visual token', () => { - tokensContainer.innerHTML = ` - <li class="js-visual-token filtered-search-token"> - <div class="name">Author</div> - </li> - `; + tokensContainer.innerHTML = FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author'); const { lastVisualToken, isLastVisualTokenValid } = gl.FilteredSearchVisualTokens.getLastVisualToken(); @@ -55,17 +47,9 @@ require('~/filtered_search/filtered_search_visual_tokens'); it('returns when there are multiple visual tokens', () => { tokensContainer.innerHTML = ` - <li class="js-visual-token filtered-search-token"> - <div class="name">label</div> - <div class="value">~bug</div> - </li> - <li class="js-visual-token filtered-search-term"> - <div class="name">search term</div> - </li> - <li class="js-visual-token filtered-search-token"> - <div class="name">author</div> - <div class="value">@root</div> - </li> + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')} `; const { lastVisualToken, isLastVisualTokenValid } @@ -78,16 +62,9 @@ require('~/filtered_search/filtered_search_visual_tokens'); it('returns when there are multiple visual tokens and an incomplete visual token', () => { tokensContainer.innerHTML = ` - <li class="js-visual-token filtered-search-token"> - <div class="name">label</div> - <div class="value">~bug</div> - </li> - <li class="js-visual-token filtered-search-term"> - <div class="name">search term</div> - </li> - <li class="js-visual-token filtered-search-token"> - <div class="name">assignee</div> - </li> + ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} + ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} + ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')} `; const { lastVisualToken, isLastVisualTokenValid } @@ -182,5 +159,54 @@ require('~/filtered_search/filtered_search_visual_tokens'); expect(token.querySelector('.value')).toEqual(null); }); }); + + describe('getLastTokenPartial', () => { + it('should get last token value', () => { + const value = '~bug'; + tokensContainer.innerHTML = 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); + + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name); + }); + + it('should return empty when there are no tokens', () => { + expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(''); + }); + }); + + describe('removeLastTokenPartial', () => { + it('should remove the last token value if it exists', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"'); + + expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null); + }); + + it('should remove the last token name if there is no value', () => { + tokensContainer.innerHTML = FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone'); + + expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null); + + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null); + }); + + it('should not remove anything when there are no tokens', () => { + const html = tokensContainer.innerHTML; + gl.FilteredSearchVisualTokens.removeLastTokenPartial(); + + expect(tokensContainer.innerHTML).toEqual(html); + }); + }); }); })(); diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js.es6 b/spec/javascripts/helpers/filtered_search_spec_helper.js.es6 new file mode 100644 index 00000000000..567efdd838c --- /dev/null +++ b/spec/javascripts/helpers/filtered_search_spec_helper.js.es6 @@ -0,0 +1,28 @@ +class FilteredSearchSpecHelper { + static createFilterVisualTokenHTML(name, value) { + return ` + <li class="js-visual-token filtered-search-token"> + <div class="name">${name}</div> + <div class="value">${value}</div> + </li> + `; + } + + static createNameFilterVisualTokenHTML(name) { + return ` + <li class="js-visual-token filtered-search-token"> + <div class="name">${name}</div> + </li> + `; + } + + static createSearchVisualTokenHTML(name) { + return ` + <li class="js-visual-token filtered-search-term"> + <div class="name">${name}</div> + </li> + `; + } +} + +module.exports = FilteredSearchSpecHelper; |