From 71dc5af9ce5e25d8d3219b296e23c3ca6340451b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 4 Nov 2016 16:27:11 -0500 Subject: [PATCH 001/185] Add basic search --- app/assets/javascripts/dispatcher.js.es6 | 3 + .../filtered_search/filtered_search_bundle.js | 13 +++ .../filtered_search_manager.js.es6 | 104 ++++++++++++++++++ app/assets/stylesheets/framework/filters.scss | 24 ++++ app/views/projects/issues/index.html.haml | 6 +- .../shared/issuable/_search_bar.html.haml | 76 +++++++++++++ config/application.rb | 1 + 7 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_bundle.js create mode 100644 app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 create mode 100644 app/views/shared/issuable/_search_bar.html.haml diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 54f13e328bd..5a9ee5c7d78 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,6 +84,9 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': + if(gl.hasOwnProperty('FilteredSearchManager')) { + new gl.FilteredSearchManager(); + } Issuable.init(); new gl.IssuableBulkActions({ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js new file mode 100644 index 00000000000..656979ba82f --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -0,0 +1,13 @@ + /* eslint-disable */ + // This is a manifest file that'll be compiled into including all the files listed below. + // Add new JavaScript code in separate files in this directory and they'll automatically + // be included in the compiled file accessible from http://example.com/assets/application.js + // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the + // the compiled file. + // + /*= require_tree . */ + + (function() { + + }).call(this); + \ No newline at end of file diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 new file mode 100644 index 00000000000..797473f2044 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -0,0 +1,104 @@ +((global) => { + const TOKEN_TYPE_STRING = 'string'; + const TOKEN_TYPE_ARRAY = 'array'; + + const validTokenKeys = [{ + key: 'author', + type: 'string', + },{ + key: 'assignee', + type: 'string' + },{ + key: 'milestone', + type: 'string' + },{ + key: 'label', + type: 'array' + },]; + + class FilteredSearchManager { + constructor() { + this.bindEvents(); + this.clearTokens(); + } + + bindEvents() { + const input = document.querySelector('.filtered-search'); + + input.addEventListener('input', this.tokenize.bind(this)); + input.addEventListener('keydown', this.checkForEnter.bind(this)); + } + + clearTokens() { + this.tokens = []; + this.searchToken = ''; + } + + tokenize(event) { + // Re-calculate tokens + this.clearTokens(); + + // TODO: Current implementation does not support token values that have valid spaces in them + // Example/ label:community contribution + const input = event.target.value; + const inputs = input.split(' '); + let searchTerms = ''; + + inputs.forEach((i) => { + const colonIndex = i.indexOf(':'); + + // Check if text is a token + if (colonIndex !== -1) { + const tokenKey = i.slice(0, colonIndex).toLowerCase(); + const tokenValue = i.slice(colonIndex + 1); + + const match = validTokenKeys.filter((v) => { + return v.name === tokenKey; + })[0]; + + if (match) { + this.tokens.push = { + key: match.key, + value: tokenValue, + }; + } + } else { + searchTerms += i + ' '; + } + }, this); + + this.searchToken = searchTerms.trim(); + this.printTokens(); + } + + printTokens() { + console.log(this.tokens); + console.log(this.searchToken); + } + + checkForEnter(event) { + if (event.key === 'Enter') { + event.stopPropagation(); + event.preventDefault(); + this.search(); + } + } + + search() { + console.log('search'); + let path = '?scope=all&state=opened&utf8=✓'; + + this.tokens.foreach((token) => { + + }); + + if (this.searchToken) { + path += '&search=' + this.searchToken; + } + + window.location = path; + } + } + + global.FilteredSearchManager = FilteredSearchManager; +})(window.gl || (window.gl = {})); \ No newline at end of file diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 19827943385..a565642ba38 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -23,3 +23,27 @@ } } +.filtered-search-container { + display: flex; +} + +.filtered-search-input-container { + display: flex; + position: relative; + width: 100%; + + .form-control { + padding-left: 25px; + + &:focus ~ .fa-filter { + color: #444; + } + } + + .fa-filter { + position: absolute; + left: 10px; + top: 10px; + color: $gray-darkest; + } +} diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 26f3f0ac292..18e8372ecab 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,6 +6,9 @@ = content_for :sub_nav do = render "projects/issues/head" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js') + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") @@ -20,7 +23,6 @@ = icon('rss') %span.icon-label Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, @@ -30,7 +32,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/search_bar', type: :issues .issues-holder = render 'issues' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml new file mode 100644 index 00000000000..40c1bd3ef98 --- /dev/null +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -0,0 +1,76 @@ +- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder +- boards_page = controller.controller_name == 'boards' + +.issues-filters + .issues-details-filters.row-content-block.second-block + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] + - if @bulk_edit + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left" + .issues-other-filters.filtered-search-container + .filtered-search-input-container + %input.form-control.filtered-search{ placeholder: 'Search or filter results...' } + = icon('filter') + .pull-right + - if boards_page + #js-boards-seach.issue-boards-search + %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } + - if can?(current_user, :admin_list, @project) + .dropdown.pull-right + %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } + Create new list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } + - if can?(current_user, :admin_label, @project) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading + - else + = render 'shared/sort_dropdown' + + - if @bulk_edit + .issues_bulk_update.hide + = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do + .filter-item.inline + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + %ul + %li + %a{href: "#", data: {id: "reopen"}} Open + %li + %a{href: "#", data: {id: "close"}} Closed + .filter-item.inline + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) + .filter-item.inline + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + .filter-item.inline + = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do + %ul + %li + %a{href: "#", data: {id: "subscribe"}} Subscribe + %li + %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe + + = hidden_field_tag 'update[issuable_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + .filter-item.inline + = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" + - has_labels = @labels && @labels.any? + .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } + - if has_labels + = render 'shared/labels_row', labels: @labels + +:javascript + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); + $('form.filter-form').on('submit', function (event) { + event.preventDefault(); + Turbolinks.visit(this.action + '&' + $(this).serialize()); + }); diff --git a/config/application.rb b/config/application.rb index 1de7fb7bdb8..aa52b0cd512 100644 --- a/config/application.rb +++ b/config/application.rb @@ -106,6 +106,7 @@ module Gitlab config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "terminal/terminal_bundle.js" + config.assets.precompile << "filtered_search/filtered_search_bundle.js" config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/*.js" config.assets.precompile << "u2f.js" From cf8ae790d13f69d15f3d279565abbba3b065fba4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:18:50 -0600 Subject: [PATCH 002/185] Add filter params to search --- .../filtered_search_manager.js.es6 | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 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 797473f2044..c26a46a8558 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -5,15 +5,19 @@ const validTokenKeys = [{ key: 'author', type: 'string', + param: 'id', },{ key: 'assignee', - type: 'string' + type: 'string', + param: 'id', },{ key: 'milestone', - type: 'string' + type: 'string', + param: 'title', },{ key: 'label', - type: 'array' + type: 'array', + param: 'name%5B%5D', },]; class FilteredSearchManager { @@ -53,14 +57,14 @@ const tokenValue = i.slice(colonIndex + 1); const match = validTokenKeys.filter((v) => { - return v.name === tokenKey; + return v.key === tokenKey; })[0]; - if (match) { - this.tokens.push = { - key: match.key, - value: tokenValue, - }; + if (match && tokenValue.length > 0) { + this.tokens.push({ + key: match.key, + value: tokenValue, + }); } } else { searchTerms += i + ' '; @@ -72,8 +76,11 @@ } printTokens() { - console.log(this.tokens); - console.log(this.searchToken); + console.log('tokens:') + this.tokens.forEach((token) => { + console.log(token); + }) + console.log('search: ' + this.searchToken); } checkForEnter(event) { @@ -88,8 +95,13 @@ console.log('search'); let path = '?scope=all&state=opened&utf8=✓'; - this.tokens.foreach((token) => { + this.tokens.forEach((token) => { + const param = validTokenKeys.find((t) => { + return t.key === token.key; + }).param; + + path += `&${token.key}_${param}=${token.value}`; }); if (this.searchToken) { From 9e8f0e63b46ad4540eb2cf8e6206ebc22200f670 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:19:15 -0600 Subject: [PATCH 003/185] Load searched params into input field --- .../filtered_search_manager.js.es6 | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 c26a46a8558..44718e8306c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -23,6 +23,7 @@ class FilteredSearchManager { constructor() { this.bindEvents(); + this.loadSearchParamsFromURL(); this.clearTokens(); } @@ -38,6 +39,31 @@ this.searchToken = ''; } + loadSearchParamsFromURL() { + const params = window.location.search.split('&'); + let inputValue = ''; + + params.forEach((p) => { + const split = p.split('='); + const key = split[0]; + const value = split[1]; + + const match = validTokenKeys.find((t) => { + return key === `${t.key}_${t.param}`; + }); + + if (match) { + const sanitizedKey = key.slice(0, key.indexOf('_')); + inputValue += `${sanitizedKey}:${value} `; + } else if (!match && key === 'search') { + inputValue += `${value} `; + } + }); + + // Trim the last space value + document.querySelector('.filtered-search').value = inputValue.trim(); + } + tokenize(event) { // Re-calculate tokens this.clearTokens(); From fc6eab6919e5cc2426328061df22e9c8985f201b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:19:30 -0600 Subject: [PATCH 004/185] Remove shared/labels_row --- app/views/shared/issuable/_search_bar.html.haml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 40c1bd3ef98..db9011d5d57 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -59,10 +59,6 @@ = hidden_field_tag :state_event, params[:state_event] .filter-item.inline = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" - - has_labels = @labels && @labels.any? - .row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) } - - if has_labels - = render 'shared/labels_row', labels: @labels :javascript new UsersSelect(); From d0165c82877cbc0ddd939713e7365337e0e5478f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 7 Nov 2016 16:33:51 -0600 Subject: [PATCH 005/185] Add author_username and assignee_username --- .../filtered_search_manager.js.es6 | 4 ++-- app/finders/issuable_finder.rb | 24 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 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 44718e8306c..94c0b99a1e1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -5,11 +5,11 @@ const validTokenKeys = [{ key: 'author', type: 'string', - param: 'id', + param: 'username', },{ key: 'assignee', type: 'string', - param: 'id', + param: 'username', },{ key: 'milestone', type: 'string', diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b4c14d05eaf..2afde8ece65 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -165,31 +165,43 @@ class IssuableFinder end end - def assignee? + def assignee_id? params[:assignee_id].present? end + def assignee_username? + params[:assignee_username].present? + end + def assignee return @assignee if defined?(@assignee) @assignee = - if assignee? && params[:assignee_id] != NONE + if assignee_id? && params[:assignee_id] != NONE User.find(params[:assignee_id]) + elsif assignee_username? && params[:assignee_username] != NONE + User.find_by(username: params[:assignee_username]) else nil end end - def author? + def author_id? params[:author_id].present? end + def author_username? + params[:author_username].present? + end + def author return @author if defined?(@author) @author = - if author? && params[:author_id] != NONE + if author_id? && params[:author_id] != NONE User.find(params[:author_id]) + elsif author_username? && params[:author_username] != NONE + User.find_by(username: params[:author_username]) else nil end @@ -263,7 +275,7 @@ class IssuableFinder end def by_assignee(items) - if assignee? + if assignee_id? || assignee_username? items = items.where(assignee_id: assignee.try(:id)) end @@ -271,7 +283,7 @@ class IssuableFinder end def by_author(items) - if author? + if author_id? || author_username? items = items.where(author_id: author.try(:id)) end From 339c5d43262d0061a70b0b485f5fe75f49a6cd0b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 11:35:28 -0600 Subject: [PATCH 006/185] Sanitize spaces in search term --- .../filtered_search/filtered_search_manager.js.es6 | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 94c0b99a1e1..c9d7a99ae44 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -56,7 +56,9 @@ const sanitizedKey = key.slice(0, key.indexOf('_')); inputValue += `${sanitizedKey}:${value} `; } else if (!match && key === 'search') { - inputValue += `${value} `; + // Sanitize value as URL converts spaces into %20 + const sanitizedValue = value.replace('%20', ' '); + inputValue += `${sanitizedValue} `; } }); @@ -139,4 +141,4 @@ } global.FilteredSearchManager = FilteredSearchManager; -})(window.gl || (window.gl = {})); \ No newline at end of file +})(window.gl || (window.gl = {})); From 823185eca1dd3483f0c178991870c9727aad6470 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 11:36:03 -0600 Subject: [PATCH 007/185] Fixed bug where search terms with colons were not searchable --- .../filtered_search/filtered_search_manager.js.es6 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 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 c9d7a99ae44..7acdabe3ef2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -76,10 +76,13 @@ const inputs = input.split(' '); let searchTerms = ''; + const addSearchTerm = function addSearchTerm(term) { + searchTerms += term + ' '; + } + inputs.forEach((i) => { const colonIndex = i.indexOf(':'); - // Check if text is a token if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); @@ -93,9 +96,11 @@ key: match.key, value: tokenValue, }); + } else { + addSearchTerm(i); } } else { - searchTerms += i + ' '; + addSearchTerm(i); } }, this); From a257f48946d9d002d829e116cc4acb6349240318 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 12:47:53 -0600 Subject: [PATCH 008/185] Add clear search button --- .../filtered_search_manager.js.es6 | 21 ++++++++++++++++++ app/assets/stylesheets/framework/filters.scss | 22 +++++++++++++++++-- .../shared/issuable/_search_bar.html.haml | 2 ++ 3 files changed, 43 insertions(+), 2 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 7acdabe3ef2..ad988fe2072 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -29,9 +29,23 @@ bindEvents() { const input = document.querySelector('.filtered-search'); + const clearSearch = document.querySelector('.clear-search'); input.addEventListener('input', this.tokenize.bind(this)); input.addEventListener('keydown', this.checkForEnter.bind(this)); + + clearSearch.addEventListener('click', this.clearSearch.bind(this)); + } + + clearSearch(event) { + event.stopPropagation(); + event.preventDefault(); + + this.clearTokens(); + const input = document.querySelector('.filtered-search'); + input.value = ''; + + event.target.classList.add('hidden'); } clearTokens() { @@ -64,12 +78,19 @@ // Trim the last space value document.querySelector('.filtered-search').value = inputValue.trim(); + + if (inputValue.trim()) { + document.querySelector('.clear-search').classList.remove('hidden'); + } } tokenize(event) { // Re-calculate tokens this.clearTokens(); + // Enable clear button + document.querySelector('.clear-search').classList.remove('hidden'); + // TODO: Current implementation does not support token values that have valid spaces in them // Example/ label:community contribution const input = event.target.value; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index a565642ba38..b192455f5f0 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -39,11 +39,29 @@ color: #444; } } - .fa-filter { position: absolute; - left: 10px; top: 10px; + left: 10px; color: $gray-darkest; } + + .fa-times { + right: 10px; + color: $gray-darkest; + } + + .clear-search { + width: 35px; + background-color: transparent; + border: none; + position: absolute; + right: 0px; + height: 100%; + outline: none; + + &:hover .fa-times { + color: #444; + } + } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index db9011d5d57..5e759301a04 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -14,6 +14,8 @@ .filtered-search-input-container %input.form-control.filtered-search{ placeholder: 'Search or filter results...' } = icon('filter') + %button.clear-search.hidden + = icon('times') .pull-right - if boards_page #js-boards-seach.issue-boards-search From 7564c5713319517d0b61bc421275197ae7c79113 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 12:59:30 -0600 Subject: [PATCH 009/185] Use + instead of %20 --- .../filtered_search/filtered_search_manager.js.es6 | 7 +++---- 1 file changed, 3 insertions(+), 4 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 ad988fe2072..fccc0de050f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -70,8 +70,8 @@ const sanitizedKey = key.slice(0, key.indexOf('_')); inputValue += `${sanitizedKey}:${value} `; } else if (!match && key === 'search') { - // Sanitize value as URL converts spaces into %20 - const sanitizedValue = value.replace('%20', ' '); + // Sanitize value as URL converts spaces into + + const sanitizedValue = value.replace(/[+]/g, ' '); inputValue += `${sanitizedValue} `; } }); @@ -149,7 +149,6 @@ console.log('search'); let path = '?scope=all&state=opened&utf8=✓'; - this.tokens.forEach((token) => { const param = validTokenKeys.find((t) => { return t.key === token.key; @@ -159,7 +158,7 @@ }); if (this.searchToken) { - path += '&search=' + this.searchToken; + path += '&search=' + this.searchToken.replace(/ /g, '+'); } window.location = path; From 6b4358eaf70afdd79e441501a5b41690ef70b845 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 13:20:37 -0600 Subject: [PATCH 010/185] Add search based on state --- .../filtered_search_manager.js.es6 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 fccc0de050f..63cdcecdf49 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -147,7 +147,22 @@ search() { console.log('search'); - let path = '?scope=all&state=opened&utf8=✓'; + let path = '?scope=all&utf8=✓'; + + // Check current state + const currentPath = window.location.search; + const stateIndex = currentPath.indexOf('state='); + const defaultState = 'opened'; + let currentState = defaultState; + + if (stateIndex !== -1) { + const remaining = currentPath.slice(stateIndex + 6); + const separatorIndex = remaining.indexOf('&'); + + currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); + } + + path += `&state=${currentState}` this.tokens.forEach((token) => { const param = validTokenKeys.find((t) => { From 7b382af73956518b73872c9638754e86da15d915 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 14:18:46 -0600 Subject: [PATCH 011/185] Add support for quotations --- .../filtered_search_manager.js.es6 | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 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 63cdcecdf49..f5e53d075b0 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -68,7 +68,13 @@ if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); - inputValue += `${sanitizedKey}:${value} `; + let sanitizedValue = value; + + if (match && sanitizedKey === 'label') { + sanitizedValue = sanitizedValue.replace(/%20/g, ' '); + } + + inputValue += `${sanitizedKey}:${sanitizedValue} `; } else if (!match && key === 'search') { // Sanitize value as URL converts spaces into + const sanitizedValue = value.replace(/[+]/g, ' '); @@ -91,26 +97,51 @@ // Enable clear button document.querySelector('.clear-search').classList.remove('hidden'); - // TODO: Current implementation does not support token values that have valid spaces in them - // Example/ label:community contribution const input = event.target.value; const inputs = input.split(' '); let searchTerms = ''; + let lastQuotation = ''; + let incompleteToken = false; const addSearchTerm = function addSearchTerm(term) { searchTerms += term + ' '; } inputs.forEach((i) => { + if (incompleteToken) { + const prevToken = this.tokens[this.tokens.length - 1]; + prevToken.value += ` ${i}`; + + // Remove last quotation + const lastQuotationRegex = new RegExp(lastQuotation, 'g'); + prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); + this.tokens[this.tokens.length - 1] = prevToken; + + // Check to see if this quotation completes the token value + if (i.indexOf(lastQuotation)) { + incompleteToken = !incompleteToken; + } + + return; + } + const colonIndex = i.indexOf(':'); if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); - const match = validTokenKeys.filter((v) => { + const match = validTokenKeys.find((v) => { return v.key === tokenKey; - })[0]; + }); + + if (tokenValue.indexOf('"') !== -1) { + lastQuotation = '"'; + incompleteToken = true; + } else if (tokenValue.indexOf('\'') !== -1) { + lastQuotation = '\''; + incompleteToken = true; + } if (match && tokenValue.length > 0) { this.tokens.push({ From 53b4d1b3a76f2aa80070699f623c90e4f7766506 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 15:42:15 -0600 Subject: [PATCH 012/185] Add special character encoding --- .../filtered_search/filtered_search_manager.js.es6 | 9 +++++---- 1 file changed, 5 insertions(+), 4 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 f5e53d075b0..393e0b8a4b2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -17,7 +17,7 @@ },{ key: 'label', type: 'array', - param: 'name%5B%5D', + param: 'name[]', },]; class FilteredSearchManager { @@ -54,13 +54,14 @@ } loadSearchParamsFromURL() { + // We can trust that each param has one & since values containing & will be encoded const params = window.location.search.split('&'); let inputValue = ''; params.forEach((p) => { const split = p.split('='); const key = split[0]; - const value = split[1]; + const value = decodeURIComponent(split[1]); const match = validTokenKeys.find((t) => { return key === `${t.key}_${t.param}`; @@ -200,11 +201,11 @@ return t.key === token.key; }).param; - path += `&${token.key}_${param}=${token.value}`; + path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; }); if (this.searchToken) { - path += '&search=' + this.searchToken.replace(/ /g, '+'); + path += '&search=' + encodeURIComponent(this.searchToken.replace(/ /g, '+')); } window.location = path; From 3ce7f23a855e9061e8f702ae9c9c07ce91e24738 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 16:20:47 -0600 Subject: [PATCH 013/185] Fix bug where search terms would not work after switching to another state tab --- .../filtered_search_manager.js.es6 | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 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 393e0b8a4b2..3528d9da88c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -55,12 +55,13 @@ loadSearchParamsFromURL() { // We can trust that each param has one & since values containing & will be encoded - const params = window.location.search.split('&'); + // Remove the first character of search as it is always ? + const params = window.location.search.slice(1).split('&'); let inputValue = ''; params.forEach((p) => { const split = p.split('='); - const key = split[0]; + const key = decodeURIComponent(split[0]); const value = decodeURIComponent(split[1]); const match = validTokenKeys.find((t) => { @@ -69,17 +70,24 @@ if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); - let sanitizedValue = value; + const valueHasSpace = value.indexOf(' ') !== -1; - if (match && sanitizedKey === 'label') { - sanitizedValue = sanitizedValue.replace(/%20/g, ' '); + const preferredQuotations = '"'; + let quotationsToUse = preferredQuotations; + + if (valueHasSpace) { + // Prefer ", but use ' if required + quotationsToUse = value.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; } - inputValue += `${sanitizedKey}:${sanitizedValue} `; + inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${value}${quotationsToUse}` : `${sanitizedKey}:${value}`; + inputValue += ' '; + } else if (!match && key === 'search') { // Sanitize value as URL converts spaces into + const sanitizedValue = value.replace(/[+]/g, ' '); - inputValue += `${sanitizedValue} `; + inputValue += sanitizedValue; + inputValue += ' '; } }); From d797b03b98e9eccc5d2c7ba4de2d46b0aff1ff67 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 16:30:06 -0600 Subject: [PATCH 014/185] Fix bug where spaces would conver into + for all values --- .../filtered_search_manager.js.es6 | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 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 3528d9da88c..9fe70bbf3a7 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -62,7 +62,11 @@ params.forEach((p) => { const split = p.split('='); const key = decodeURIComponent(split[0]); - const value = decodeURIComponent(split[1]); + const value = split[1]; + + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; const match = validTokenKeys.find((t) => { return key === `${t.key}_${t.param}`; @@ -70,22 +74,20 @@ if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = value.indexOf(' ') !== -1; + const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; const preferredQuotations = '"'; let quotationsToUse = preferredQuotations; if (valueHasSpace) { // Prefer ", but use ' if required - quotationsToUse = value.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; } - inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${value}${quotationsToUse}` : `${sanitizedKey}:${value}`; + inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${sanitizedValue}`; inputValue += ' '; } else if (!match && key === 'search') { - // Sanitize value as URL converts spaces into + - const sanitizedValue = value.replace(/[+]/g, ' '); inputValue += sanitizedValue; inputValue += ' '; } @@ -213,7 +215,7 @@ }); if (this.searchToken) { - path += '&search=' + encodeURIComponent(this.searchToken.replace(/ /g, '+')); + path += '&search=' + encodeURIComponent(this.searchToken); } window.location = path; From ad02257c3ca25806c7104673566bc99c3f6867ed Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 8 Nov 2016 17:54:19 -0600 Subject: [PATCH 015/185] Fix bug where clear search button would not toggle visible --- .../filtered_search_manager.js.es6 | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 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 9fe70bbf3a7..42fe0cace10 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -32,6 +32,7 @@ const clearSearch = document.querySelector('.clear-search'); input.addEventListener('input', this.tokenize.bind(this)); + input.addEventListener('input', this.toggleClearSearchButton); input.addEventListener('keydown', this.checkForEnter.bind(this)); clearSearch.addEventListener('click', this.clearSearch.bind(this)); @@ -42,10 +43,8 @@ event.preventDefault(); this.clearTokens(); - const input = document.querySelector('.filtered-search'); - input.value = ''; - - event.target.classList.add('hidden'); + document.querySelector('.filtered-search').value = ''; + document.querySelector('.clear-search').classList.add('hidden'); } clearTokens() { @@ -101,13 +100,20 @@ } } + toggleClearSearchButton(event) { + const clearSearch = document.querySelector('.clear-search'); + + if (event.target.value) { + clearSearch.classList.remove('hidden'); + } else { + clearSearch.classList.add('hidden'); + } + } + tokenize(event) { // Re-calculate tokens this.clearTokens(); - // Enable clear button - document.querySelector('.clear-search').classList.remove('hidden'); - const input = event.target.value; const inputs = input.split(' '); let searchTerms = ''; From fe4d33cf15b877e8ba22f518f068088db8a3e36d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 13:51:43 -0600 Subject: [PATCH 016/185] Fix scss lint --- app/assets/stylesheets/framework/filters.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b192455f5f0..90b9394b207 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -39,6 +39,7 @@ color: #444; } } + .fa-filter { position: absolute; top: 10px; @@ -56,7 +57,7 @@ background-color: transparent; border: none; position: absolute; - right: 0px; + right: 0; height: 100%; outline: none; From f1574e45b268e9f1dd488a3962327a4c40f26ae9 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 14:31:58 -0600 Subject: [PATCH 017/185] Fix ESLint errors --- .../filtered_search_manager.js.es6 | 156 ++++++++---------- 1 file changed, 71 insertions(+), 85 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 42fe0cace10..1b58fc01608 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,29 +1,81 @@ +/* eslint-disable no-param-reassign */ ((global) => { - const TOKEN_TYPE_STRING = 'string'; - const TOKEN_TYPE_ARRAY = 'array'; - const validTokenKeys = [{ key: 'author', type: 'string', param: 'username', - },{ + }, { key: 'assignee', type: 'string', param: 'username', - },{ + }, { key: 'milestone', type: 'string', param: 'title', - },{ + }, { key: 'label', type: 'array', param: 'name[]', - },]; + }]; + + function toggleClearSearchButton(event) { + const clearSearch = document.querySelector('.clear-search'); + + if (event.target.value) { + clearSearch.classList.remove('hidden'); + } else { + clearSearch.classList.add('hidden'); + } + } + + function loadSearchParamsFromURL() { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + const params = window.location.search.slice(1).split('&'); + let inputValue = ''; + + params.forEach((p) => { + const split = p.split('='); + const key = decodeURIComponent(split[0]); + const value = split[1]; + + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; + const match = validTokenKeys.find(t => key === `${t.key}_${t.param}`); + + if (match) { + const sanitizedKey = key.slice(0, key.indexOf('_')); + const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; + + const preferredQuotations = '"'; + let quotationsToUse = preferredQuotations; + + if (valueHasSpace) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + } + + inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${sanitizedValue}`; + inputValue += ' '; + } else if (!match && key === 'search') { + inputValue += sanitizedValue; + inputValue += ' '; + } + }); + + // Trim the last space value + document.querySelector('.filtered-search').value = inputValue.trim(); + + if (inputValue.trim()) { + document.querySelector('.clear-search').classList.remove('hidden'); + } + } class FilteredSearchManager { constructor() { this.bindEvents(); - this.loadSearchParamsFromURL(); + loadSearchParamsFromURL(); this.clearTokens(); } @@ -32,7 +84,7 @@ const clearSearch = document.querySelector('.clear-search'); input.addEventListener('input', this.tokenize.bind(this)); - input.addEventListener('input', this.toggleClearSearchButton); + input.addEventListener('input', toggleClearSearchButton); input.addEventListener('keydown', this.checkForEnter.bind(this)); clearSearch.addEventListener('click', this.clearSearch.bind(this)); @@ -52,64 +104,6 @@ this.searchToken = ''; } - loadSearchParamsFromURL() { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - const params = window.location.search.slice(1).split('&'); - let inputValue = ''; - - params.forEach((p) => { - const split = p.split('='); - const key = decodeURIComponent(split[0]); - const value = split[1]; - - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; - - const match = validTokenKeys.find((t) => { - return key === `${t.key}_${t.param}`; - }); - - if (match) { - const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; - - const preferredQuotations = '"'; - let quotationsToUse = preferredQuotations; - - if (valueHasSpace) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; - } - - inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${sanitizedValue}`; - inputValue += ' '; - - } else if (!match && key === 'search') { - inputValue += sanitizedValue; - inputValue += ' '; - } - }); - - // Trim the last space value - document.querySelector('.filtered-search').value = inputValue.trim(); - - if (inputValue.trim()) { - document.querySelector('.clear-search').classList.remove('hidden'); - } - } - - toggleClearSearchButton(event) { - const clearSearch = document.querySelector('.clear-search'); - - if (event.target.value) { - clearSearch.classList.remove('hidden'); - } else { - clearSearch.classList.add('hidden'); - } - } - tokenize(event) { // Re-calculate tokens this.clearTokens(); @@ -121,8 +115,9 @@ let incompleteToken = false; const addSearchTerm = function addSearchTerm(term) { - searchTerms += term + ' '; - } + // Add space for next term + searchTerms += `${term} `; + }; inputs.forEach((i) => { if (incompleteToken) { @@ -147,10 +142,7 @@ if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); - - const match = validTokenKeys.find((v) => { - return v.key === tokenKey; - }); + const match = validTokenKeys.find(v => v.key === tokenKey); if (tokenValue.indexOf('"') !== -1) { lastQuotation = '"'; @@ -178,11 +170,9 @@ } printTokens() { - console.log('tokens:') - this.tokens.forEach((token) => { - console.log(token); - }) - console.log('search: ' + this.searchToken); + console.log('tokens:'); + this.tokens.forEach(token => console.log(token)); + console.log(`search: ${this.searchToken}`); } checkForEnter(event) { @@ -210,18 +200,14 @@ currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); } - path += `&state=${currentState}` - + path += `&state=${currentState}`; this.tokens.forEach((token) => { - const param = validTokenKeys.find((t) => { - return t.key === token.key; - }).param; - + const param = validTokenKeys.find(t => t.key === token.key).param; path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; }); if (this.searchToken) { - path += '&search=' + encodeURIComponent(this.searchToken); + path += `&search=${encodeURIComponent(this.searchToken)}`; } window.location = path; From 3845bf377296f58e1604d44e4db529099e14888e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 14:44:11 -0600 Subject: [PATCH 018/185] Add droplab --- app/assets/javascripts/droplab/droplab.js | 515 ++++++++++++++++++ .../javascripts/droplab/droplab_ajax.js | 45 ++ .../javascripts/droplab/droplab_filter.js | 28 + 3 files changed, 588 insertions(+) create mode 100644 app/assets/javascripts/droplab/droplab.js create mode 100644 app/assets/javascripts/droplab/droplab_ajax.js create mode 100644 app/assets/javascripts/droplab/droplab_filter.js diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js new file mode 100644 index 00000000000..18ca8be7203 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab.js @@ -0,0 +1,515 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0){ + if(!listItems[currentIndex-1]){ + currentIndex = currentIndex-1; + } + listItems[currentIndex-1].classList.add('dropdown-active'); + } + }; + + var mousedown = function mousedown(e) { + var list = e.detail.hook.list; + removeHighlight(list); + list.show(); + currentIndex = 0; + isUpArrow = false; + isDownArrow = false; + }; + var selectItem = function selectItem(list) { + var listItems = removeHighlight(list); + var currentItem = listItems[currentIndex-1]; + var listEvent = new CustomEvent('click.dl', { + detail: { + list: list, + selected: currentItem, + data: currentItem.dataset, + }, + }); + list.list.dispatchEvent(listEvent); + list.hide(); + } + + var keydown = function keydown(e){ + var typedOn = e.target; + isUpArrow = false; + isDownArrow = false; + + if(e.detail.which){ + currentKey = e.detail.which; + if(currentKey === 13){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 38) { + isUpArrow = true; + } + if(currentKey === 40) { + isDownArrow = true; + } + } else if(e.detail.key) { + currentKey = e.detail.key; + if(currentKey === 'Enter'){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 'ArrowUp') { + isUpArrow = true; + } + if(currentKey === 'ArrowDown') { + isDownArrow = true; + } + } + if(isUpArrow){ currentIndex--; } + if(isDownArrow){ currentIndex++; } + if(currentIndex < 0){ currentIndex = 0; } + setMenuForArrows(e.detail.hook.list); + }; + + w.addEventListener('mousedown.dl', mousedown); + w.addEventListener('keydown.dl', keydown); + }; +}); +},{"./window":11}],10:[function(require,module,exports){ +var DATA_TRIGGER = require('./constants').DATA_TRIGGER; +var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN; + +var toDataCamelCase = function(attr){ + return this.camelize(attr.split('-').slice(1).join(' ')); +}; + +// the tiniest damn templating I can do +var t = function(s,d){ + for(var p in d) + s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]); + return s; +}; + +var camelize = function(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) { + return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); + }).replace(/\s+/g, ''); +}; + +var closest = function(thisTag, stopTag) { + while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + thisTag = thisTag.parentNode; + } + return thisTag; +}; + +var isDropDownParts = function(target) { + if(target.tagName === 'HTML') { return false; } + return ( + target.hasAttribute(DATA_TRIGGER) || + target.hasAttribute(DATA_DROPDOWN) + ); +}; + +module.exports = { + toDataCamelCase: toDataCamelCase, + t: t, + camelize: camelize, + closest: closest, + isDropDownParts: isDropDownParts, +}; + +},{"./constants":1}],11:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[8])(8) +}); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js new file mode 100644 index 00000000000..23e43b352d6 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -0,0 +1,45 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Wed, 9 Nov 2016 17:07:30 -0600 Subject: [PATCH 019/185] Refactor tokenizer --- .../filtered_search_manager.js.es6 | 120 ++++-------------- .../filtered_search_tokenizer.es6 | 90 +++++++++++++ 2 files changed, 115 insertions(+), 95 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 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 1b58fc01608..58c64ea078d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -18,13 +18,21 @@ param: 'name[]', }]; + function clearSearch(event) { + event.stopPropagation(); + event.preventDefault(); + + document.querySelector('.filtered-search').value = ''; + document.querySelector('.clear-search').classList.add('hidden'); + } + function toggleClearSearchButton(event) { - const clearSearch = document.querySelector('.clear-search'); + const clearSearchButton = document.querySelector('.clear-search'); if (event.target.value) { - clearSearch.classList.remove('hidden'); + clearSearchButton.classList.remove('hidden'); } else { - clearSearch.classList.add('hidden'); + clearSearchButton.classList.add('hidden'); } } @@ -74,105 +82,24 @@ class FilteredSearchManager { constructor() { + this.tokenizer = new gl.FilteredSearchTokenizer(validTokenKeys); this.bindEvents(); loadSearchParamsFromURL(); - this.clearTokens(); } bindEvents() { - const input = document.querySelector('.filtered-search'); - const clearSearch = document.querySelector('.clear-search'); + const filteredSearchInput = document.querySelector('.filtered-search'); - input.addEventListener('input', this.tokenize.bind(this)); - input.addEventListener('input', toggleClearSearchButton); - input.addEventListener('keydown', this.checkForEnter.bind(this)); + filteredSearchInput.addEventListener('input', this.processInput.bind(this)); + filteredSearchInput.addEventListener('input', toggleClearSearchButton); + filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - clearSearch.addEventListener('click', this.clearSearch.bind(this)); + document.querySelector('.clear-search').addEventListener('click', clearSearch); } - clearSearch(event) { - event.stopPropagation(); - event.preventDefault(); - - this.clearTokens(); - document.querySelector('.filtered-search').value = ''; - document.querySelector('.clear-search').classList.add('hidden'); - } - - clearTokens() { - this.tokens = []; - this.searchToken = ''; - } - - tokenize(event) { - // Re-calculate tokens - this.clearTokens(); - + processInput(event) { const input = event.target.value; - const inputs = input.split(' '); - let searchTerms = ''; - let lastQuotation = ''; - let incompleteToken = false; - - const addSearchTerm = function addSearchTerm(term) { - // Add space for next term - searchTerms += `${term} `; - }; - - inputs.forEach((i) => { - if (incompleteToken) { - const prevToken = this.tokens[this.tokens.length - 1]; - prevToken.value += ` ${i}`; - - // Remove last quotation - const lastQuotationRegex = new RegExp(lastQuotation, 'g'); - prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); - this.tokens[this.tokens.length - 1] = prevToken; - - // Check to see if this quotation completes the token value - if (i.indexOf(lastQuotation)) { - incompleteToken = !incompleteToken; - } - - return; - } - - const colonIndex = i.indexOf(':'); - - if (colonIndex !== -1) { - const tokenKey = i.slice(0, colonIndex).toLowerCase(); - const tokenValue = i.slice(colonIndex + 1); - const match = validTokenKeys.find(v => v.key === tokenKey); - - if (tokenValue.indexOf('"') !== -1) { - lastQuotation = '"'; - incompleteToken = true; - } else if (tokenValue.indexOf('\'') !== -1) { - lastQuotation = '\''; - incompleteToken = true; - } - - if (match && tokenValue.length > 0) { - this.tokens.push({ - key: match.key, - value: tokenValue, - }); - } else { - addSearchTerm(i); - } - } else { - addSearchTerm(i); - } - }, this); - - this.searchToken = searchTerms.trim(); - this.printTokens(); - } - - printTokens() { - console.log('tokens:'); - this.tokens.forEach(token => console.log(token)); - console.log(`search: ${this.searchToken}`); + this.tokenizer.processTokens(input); } checkForEnter(event) { @@ -193,6 +120,9 @@ const defaultState = 'opened'; let currentState = defaultState; + const tokens = this.tokenizer.getTokens(); + const searchToken = this.tokenizer.getSearchToken(); + if (stateIndex !== -1) { const remaining = currentPath.slice(stateIndex + 6); const separatorIndex = remaining.indexOf('&'); @@ -201,13 +131,13 @@ } path += `&state=${currentState}`; - this.tokens.forEach((token) => { + tokens.forEach((token) => { const param = validTokenKeys.find(t => t.key === token.key).param; path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; }); - if (this.searchToken) { - path += `&search=${encodeURIComponent(this.searchToken)}`; + if (searchToken) { + path += `&search=${encodeURIComponent(searchToken)}`; } window.location = path; diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 new file mode 100644 index 00000000000..f6cc1b8860d --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -0,0 +1,90 @@ +/* eslint-disable no-param-reassign */ +((global) => { + class FilteredSearchTokenizer { + constructor(validTokenKeys) { + this.validTokenKeys = validTokenKeys; + this.resetTokens(); + } + + getTokens() { + return this.tokens; + } + + getSearchToken() { + return this.searchToken; + } + + resetTokens() { + this.tokens = []; + this.searchToken = ''; + } + + printTokens() { + console.log('tokens:'); + this.tokens.forEach(token => console.log(token)); + console.log(`search: ${this.searchToken}`); + } + + processTokens(input) { + // Re-calculate tokens + this.resetTokens(); + + const inputs = input.split(' '); + let searchTerms = ''; + let lastQuotation = ''; + let incompleteToken = false; + + inputs.forEach((i) => { + if (incompleteToken) { + const prevToken = this.tokens[this.tokens.length - 1]; + prevToken.value += ` ${i}`; + + // Remove last quotation + const lastQuotationRegex = new RegExp(lastQuotation, 'g'); + prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); + this.tokens[this.tokens.length - 1] = prevToken; + + // Check to see if this quotation completes the token value + if (i.indexOf(lastQuotation)) { + incompleteToken = !incompleteToken; + } + + return; + } + + const colonIndex = i.indexOf(':'); + + if (colonIndex !== -1) { + const tokenKey = i.slice(0, colonIndex).toLowerCase(); + const tokenValue = i.slice(colonIndex + 1); + const match = this.validTokenKeys.find(v => v.key === tokenKey); + + if (tokenValue.indexOf('"') !== -1) { + lastQuotation = '"'; + incompleteToken = true; + } else if (tokenValue.indexOf('\'') !== -1) { + lastQuotation = '\''; + incompleteToken = true; + } + + if (match && tokenValue.length > 0) { + this.tokens.push({ + key: match.key, + value: tokenValue, + }); + + return; + } + } + + // Add space for next term + searchTerms += `${i} `; + }, this); + + this.searchToken = searchTerms.trim(); + this.printTokens(); + } + } + + global.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(window.gl || (window.gl = {})); From 8b4e4e333db0cf47080aa8577b4351b9e00525ea Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 9 Nov 2016 19:10:15 -0600 Subject: [PATCH 020/185] Fix JS for tests --- .../filtered_search/filtered_search_manager.js.es6 | 7 ++++--- .../filtered_search/filtered_search_tokenizer.es6 | 2 +- 2 files changed, 5 insertions(+), 4 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 58c64ea078d..db414b9755d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -50,7 +50,7 @@ // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; - const match = validTokenKeys.find(t => key === `${t.key}_${t.param}`); + const match = validTokenKeys.filter(t => key === `${t.key}_${t.param}`)[0]; if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); @@ -103,7 +103,8 @@ } checkForEnter(event) { - if (event.key === 'Enter') { + // Enter KeyCode + if (event.keyCode === 13) { event.stopPropagation(); event.preventDefault(); this.search(); @@ -132,7 +133,7 @@ path += `&state=${currentState}`; tokens.forEach((token) => { - const param = validTokenKeys.find(t => t.key === token.key).param; + const param = validTokenKeys.filter(t => t.key === token.key)[0].param; path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; }); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index f6cc1b8860d..ddb173b2d98 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -57,7 +57,7 @@ if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); - const match = this.validTokenKeys.find(v => v.key === tokenKey); + const match = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; if (tokenValue.indexOf('"') !== -1) { lastQuotation = '"'; From 9c8a86f60d2d36b628c5275004e4c17aa07aeeeb Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 10 Nov 2016 16:49:12 -0600 Subject: [PATCH 021/185] Update filter issue specs --- app/assets/stylesheets/framework/filters.scss | 2 + spec/features/issues/filter_issues_spec.rb | 542 ++++++++++-------- 2 files changed, 291 insertions(+), 253 deletions(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 90b9394b207..c679a3833e9 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -24,10 +24,12 @@ } .filtered-search-container { + display: -webkit-flex; display: flex; } .filtered-search-input-container { + display: -webkit-flex; display: flex; position: relative; width: 100%; diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 0d19563d628..7d681742045 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -4,147 +4,236 @@ describe 'Filter issues', feature: true do include WaitForAjax let!(:group) { create(:group) } - let!(:project) { create(:project, group: group) } + let!(:project) { create(:project) } let!(:user) { create(:user)} + let!(:user) { create(:user) } + let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } + let!(:milestone) { create(:milestone, title: "8", project: project) } + + def input_filtered_search(search_term) + filtered_search = find('.filtered-search') + filtered_search.set(search_term) + filtered_search.send_keys(:enter) + end + + def expect_no_issues_list + page.within '.issues-list' do + expect(page).not_to have_selector('.issue') + end + end + + def expect_issues_list_count(open_count, closed_count = 0) + all_count = open_count + closed_count + + expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: open_count) + end + end + before do project.team << [user, :master] + project.team << [user2, :master] group.add_developer(user) + group.add_developer(user2) login_as(user) create(:issue, project: project) + + create(:issue, title: "Bug report 1", project: project) + create(:issue, title: "Bug report 2", project: project) + create(:issue, title: "issue with 'single quotes'", project: project) + create(:issue, title: "issue with \"double quotes\"", project: project) + create(:issue, title: "issue with !@\#{$%^&*()-+", project: project) + create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user) + create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user) + + issue = create(:issue, + title: "Bug 2", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue.labels << bug_label + + issue_with_caps_label = create(:issue, + title: "issue by assignee with searchTerm and label", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_caps_label.labels << caps_sensitive_label + + issue_with_everything = create(:issue, + title: "Bug report with everything you thought was possible", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_everything.labels << bug_label + issue_with_everything.labels << caps_sensitive_label + + visit namespace_project_issues_path(project.namespace, project) end - describe 'for assignee from issues#index' do - before do - visit namespace_project_issues_path(project.namespace, project) + describe 'filter issues by author' do + context 'only author', js: true do + it 'filters issues by searched author' do + input_filtered_search("author:#{user.username}") + expect_issues_list_count(5) + end - find('.js-assignee-search').click + it 'filters issues by invalid author' do + # YOLO + end - find('.dropdown-menu-user-link', text: user.username).click - - wait_for_ajax + it 'filters issues by multiple authors' do + # YOLO + end end - context 'assignee', js: true do - it 'updates to current user' do - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + context 'author with other filters', js: true do + it 'filters issues by searched author and text' do + input_filtered_search("author:#{user.username} issue") + expect_issues_list_count(3) end - it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click - - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + it 'filters issues by searched author, assignee and text' do + input_filtered_search("author:#{user.username} assignee:#{user.username} issue") + expect_issues_list_count(3) end - it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click - - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + it 'filters issues by searched author, assignee, label, and text' do + input_filtered_search("author:#{user.username} assignee:#{user.username} label:#{caps_sensitive_label.title} issue") + expect_issues_list_count(1) end + + it 'filters issues by searched author, assignee, label, milestone and text' do + input_filtered_search("author:#{user.username} assignee:#{user.username} label:#{caps_sensitive_label.title} milestone:#{milestone.title} issue") + expect_issues_list_count(1) + end + end + + context 'sorting', js: true do + # TODO end end - describe 'for milestone from issues#index' do - before do - visit namespace_project_issues_path(project.namespace, project) + describe 'filter issues by assignee' do + context 'only assignee', js: true do + it 'filters issues by searched assignee' do + input_filtered_search("assignee:#{user.username}") + expect_issues_list_count(5) + end - find('.js-milestone-select').click + it 'filters issues by no assignee' do + # TODO + end - find('.milestone-filter .dropdown-content a', text: milestone.title).click + it 'filters issues by invalid assignee' do + # YOLO + end - wait_for_ajax + it 'filters issues by multiple assignees' do + # YOLO + end end - context 'milestone', js: true do - it 'updates to current milestone' do - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + context 'assignee with other filters', js: true do + it 'filters issues by searched assignee and text' do + input_filtered_search("assignee:#{user.username} searchTerm") + expect_issues_list_count(2) end - it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click - - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + it 'filters issues by searched assignee, author and text' do + input_filtered_search("assignee:#{user.username} author:#{user.username} searchTerm") + expect_issues_list_count(2) end - it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click - - expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + it 'filters issues by searched assignee, author, label, text' do + input_filtered_search("assignee:#{user.username} author:#{user.username} label:#{caps_sensitive_label.title} searchTerm") + expect_issues_list_count(1) end + + it 'filters issues by searched assignee, author, label, milestone and text' do + input_filtered_search("assignee:#{user.username} author:#{user.username} label:#{caps_sensitive_label.title} milestone:#{milestone.title} searchTerm") + expect_issues_list_count(1) + end + end + + context 'sorting', js: true do + # TODO end end - describe 'for label from issues#index', js: true do - before do - visit namespace_project_issues_path(project.namespace, project) - find('.js-label-select').click - wait_for_ajax - end - - it 'filters by any label' do - find('.dropdown-menu-labels a', text: 'Any Label').click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax - - expect(find('.labels-filter')).to have_content 'Label' - end - - it 'filters by no label' do - find('.dropdown-menu-labels a', text: 'No Label').click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - wait_for_ajax - - page.within '.labels-filter' do - expect(page).to have_content 'Labels' - end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels') - end - - it 'filters by a label' do - find('.dropdown-menu-labels a', text: label.title).click - page.within '.labels-filter' do - expect(page).to have_content label.title - end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) - end - - it "filters by `won't fix` and another label" do - page.within '.labels-filter' do - click_link wontfix.title - expect(page).to have_content wontfix.title - click_link label.title + describe 'filter issues by label' do + context 'only label', js: true do + it 'filters issues by searched label' do + input_filtered_search("label:#{bug_label.title}") + expect_issues_list_count(2) end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more") - end - - it "filters by `won't fix` label followed by another label after page load" do - page.within '.labels-filter' do - click_link wontfix.title - expect(page).to have_content wontfix.title + it 'filters issues by no label' do + # TODO end - find('.dropdown-menu-close-icon').click + it 'filters issues by invalid label' do + # YOLO + end - expect(find('.filtered-labels')).to have_content(wontfix.title) + it 'filters issues by multiple labels' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title}") + expect_issues_list_count(1) + end + end - find('.js-label-select').click - wait_for_ajax - find('.dropdown-menu-labels a', text: label.title).click + context 'label with other filters', js: true do + it 'filters issues by searched label and text' do + input_filtered_search("label:#{caps_sensitive_label.title} bug") + expect_issues_list_count(1) + end - find('.dropdown-menu-close-icon').click + it 'filters issues by searched label, author and text' do + input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} bug") + expect_issues_list_count(1) + end - expect(find('.filtered-labels')).to have_content(wontfix.title) - expect(find('.filtered-labels')).to have_content(label.title) + it 'filters issues by searched label, author, assignee and text' do + input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} bug") + expect_issues_list_count(1) + end - find('.js-label-select').click - wait_for_ajax + it 'filters issues by searched label, author, assignee, milestone and text' do + input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} milestone:#{milestone.title} bug") + expect_issues_list_count(1) + end + end - expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active') - expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active') + context 'multiple labels with other filters', js: true do + it 'filters issues by searched label, label2, and text' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} bug") + expect_issues_list_count(1) + end + + it 'filters issues by searched label, label2, author and text' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} bug") + expect_issues_list_count(1) + end + + it 'filters issues by searched label, label2, author, assignee and text' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} bug") + expect_issues_list_count(1) + end + + it 'filters issues by searched label, label2, author, assignee, milestone and text' do + input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} milestone:#{milestone.title} bug") + expect_issues_list_count(1) + end end it "selects and unselects `won't fix`" do @@ -153,211 +242,160 @@ describe 'Filter issues', feature: true do find('.dropdown-menu-close-icon').click expect(page).not_to have_css('.filtered-labels') + context 'sorting', js: true do + # TODO end end - describe 'for assignee and label from issues#index' do - before do - visit namespace_project_issues_path(project.namespace, project) + describe 'filter issues by milestone' do + context 'only milestone', js: true do + it 'filters issues by searched milestone' do + input_filtered_search("milestone:#{milestone.title}") + expect_issues_list_count(5) + end - find('.js-assignee-search').click + it 'filters issues by no milestone' do + # TODO + end - find('.dropdown-menu-user-link', text: user.username).click + it 'filters issues by upcoming milestones' do + # TODO + end - expect(page).not_to have_selector('.issues-list .issue') + it 'filters issues by invalid milestones' do + # YOLO + end - find('.js-label-select').click - - find('.dropdown-menu-labels .dropdown-content a', text: label.title).click - page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click - - wait_for_ajax + it 'filters issues by multiple milestones' do + # YOLO + end end - context 'assignee and label', js: true do - it 'updates to current assignee and label' do - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + context 'milestone with other filters', js: true do + it 'filters issues by searched milestone and text' do end - it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click - - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + it 'filters issues by searched milestone, author and text' do end - it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click - - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) - expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + it 'filters issues by searched milestone, author, assignee and text' do end + + it 'filters issues by searched milestone, author, assignee, label and text' do + end + end + + context 'sorting', js: true do + # TODO end end describe 'filter issues by text' do - before do - create(:issue, title: "Bug", project: project) - - bug_label = create(:label, project: project, title: 'bug') - milestone = create(:milestone, title: "8", project: project) - - issue = create(:issue, - title: "Bug 2", - project: project, - milestone: milestone, - author: user, - assignee: user) - issue.labels << bug_label - - visit namespace_project_issues_path(project.namespace, project) - end - context 'only text', js: true do it 'filters issues by searched text' do - fill_in 'issuable_search', with: 'Bug' + input_filtered_search('Bug') + expect_issues_list_count(4) + end - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end + it 'filters issues by multiple searched text' do + input_filtered_search('Bug report') + expect_issues_list_count(3) + end + + it 'filters issues by case insensitive searched text' do + input_filtered_search('bug report') + expect_issues_list_count(3) + end + + it 'filters issues by searched text containing single quotes' do + input_filtered_search('\'single quotes\'') + expect_issues_list_count(1) + end + + it 'filters issues by searched text containing double quotes' do + input_filtered_search('"double quotes"') + expect_issues_list_count(1) + end + + it 'filters issues by searched text containing special characters' do + input_filtered_search('!@#{$%^&*()-+') + expect_issues_list_count(1) end it 'does not show any issues' do - fill_in 'issuable_search', with: 'testing' - - page.within '.issues-list' do - expect(page).not_to have_selector('.issue') - end + input_filtered_search('testing') + expect_no_issues_list() end end - context 'text and dropdown options', js: true do - it 'filters by text and label' do - fill_in 'issuable_search', with: 'Bug' - - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end - - click_button 'Label' - page.within '.labels-filter' do - click_link 'bug' - end - find('.dropdown-menu-close-icon').click - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) - end + context 'searched text with other filters', js: true do + it 'filters issues by searched text and author' do + input_filtered_search("bug author:#{user.username}") + expect_issues_list_count(2) end - it 'filters by text and milestone' do - fill_in 'issuable_search', with: 'Bug' - - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end - - click_button 'Milestone' - page.within '.milestone-filter' do - click_link '8' - end - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) - end + it 'filters issues by searched text, author and more text' do + input_filtered_search("bug author:#{user.username} report") + expect_issues_list_count(1) end - it 'filters by text and assignee' do - fill_in 'issuable_search', with: 'Bug' - - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end - - click_button 'Assignee' - page.within '.dropdown-menu-assignee' do - click_link user.name - end - - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) - end + it 'filters issues by searched text, author and assignee' do + input_filtered_search("bug author:#{user.username} assignee:#{user.username}") + expect_issues_list_count(2) end - it 'filters by text and author' do - fill_in 'issuable_search', with: 'Bug' + it 'filters issues by searched text, author, more text and assignee' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username}") + expect_issues_list_count(1) + end - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end + it 'filters issues by searched text, author, more text, assignee and even more text' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with") + expect_issues_list_count(1) + end - click_button 'Author' - page.within '.dropdown-menu-author' do - click_link user.name - end + it 'filters issues by searched text, author, assignee and label' do + input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title}") + expect_issues_list_count(2) + end - expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) - end + it 'filters issues by searched text, author, text, assignee, text, label and text' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything") + expect_issues_list_count(1) + end + + it 'filters issues by searched text, author, assignee, label and milestone' do + input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title} milestone:#{milestone.title}") + expect_issues_list_count(2) + end + + it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything milestone:#{milestone.title} you") + expect_issues_list_count(1) + end + + it 'filters issues by searched text, author, assignee, multiple labels and milestone' do + input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title} label:#{caps_sensitive_label.title} milestone:#{milestone.title}") + expect_issues_list_count(1) + end + + it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do + input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything label:#{caps_sensitive_label.title} you milestone:#{milestone.title} thought") + expect_issues_list_count(1) end end - end - describe 'filter issues and sort', js: true do - before do - bug_label = create(:label, project: project, title: 'bug') - bug_one = create(:issue, title: "Frontend", project: project) - bug_two = create(:issue, title: "Bug 2", project: project) - - bug_one.labels << bug_label - bug_two.labels << bug_label - - visit namespace_project_issues_path(project.namespace, project) - end - - it 'is able to filter and sort issues' do - click_button 'Label' - wait_for_ajax - page.within '.labels-filter' do - click_link 'bug' - end - find('.dropdown-menu-close-icon').click - wait_for_ajax - - expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) - end - - click_button 'Last created' - page.within '.dropdown-menu-sort' do - click_link 'Oldest created' - end - wait_for_ajax - - page.within '.issues-list' do - expect(page).to have_content('Frontend') - end + context 'sorting', js: true do + # TODO end end it 'updates atom feed link for project issues' do visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') params = CGI::parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => ['']) expect(params).to include('assignee_id' => [user.id.to_s]) @@ -368,12 +406,10 @@ describe 'Filter issues', feature: true do it 'updates atom feed link for group issues' do visit issues_group_path(group, milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') params = CGI::parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => ['']) expect(params).to include('assignee_id' => [user.id.to_s]) From f20875ec4557b23d6df810bd49e1955f5fbbd6e0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 11:56:47 -0600 Subject: [PATCH 022/185] Add username to gon --- app/assets/javascripts/search_autocomplete.js.es6 | 7 ++++--- lib/gitlab/gon_helper.rb | 1 + spec/features/search_spec.rb | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6 index 437f5dbbf7d..cec8856d4e7 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -142,8 +142,9 @@ } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils; userId = gon.current_user_id; + userName = gon.current_username; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; if (utils.isInGroupsPage() && groupOptions) { options = groupOptions[utils.getGroupSlug()]; @@ -158,10 +159,10 @@ header: "" + name }, { text: 'Issues assigned to me', - url: issuesPath + "/?assignee_id=" + userId + url: issuesPath + "/?assignee_username=" + userName }, { text: "Issues I've created", - url: issuesPath + "/?author_id=" + userId + url: issuesPath + "/?author_username=" + userName }, 'separator', { text: 'Merge requests assigned to me', url: mrPath + "/?assignee_id=" + userId diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 4d4e04e9e35..b8a5ac907a4 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -13,6 +13,7 @@ module Gitlab if current_user gon.current_user_id = current_user.id + gon.current_username = current_user.username end end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index caecd027aaa..9a7079848a5 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -169,16 +169,16 @@ describe "Search", feature: true do find('.dropdown-menu').click_link 'Issues assigned to me' sleep 2 - expect(page).to have_selector('.issues-holder') - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(page).to have_selector('.filtered-search') + expect(find('.filtered-search').value).to eq("assignee:#{user.username}") end it 'takes user to her issues page when issues authored is clicked' do find('.dropdown-menu').click_link "Issues I've created" sleep 2 - expect(page).to have_selector('.issues-holder') - expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) + expect(page).to have_selector('.filtered-search') + expect(find('.filtered-search').value).to eq("author:#{user.username}") end it 'takes user to her MR page when MR assigned is clicked' do From 08728cb3262a4ed9e98920b5f308e84ca6c5749a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 14:51:32 -0600 Subject: [PATCH 023/185] Move spec to check on MR page instead of Issues page --- .../filter_by_labels_spec.rb | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) rename spec/features/{issues => merge_requests}/filter_by_labels_spec.rb (83%) diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb similarity index 83% rename from spec/features/issues/filter_by_labels_spec.rb rename to spec/features/merge_requests/filter_by_labels_spec.rb index 0253629f753..eff350ed53a 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -7,25 +7,27 @@ feature 'Issue filtering by Labels', feature: true, js: true do let!(:user) { create(:user) } let!(:label) { create(:label, project: project) } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:enhancement) { create(:label, project: project, title: 'enhancement') } + + let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } + let!(:mr2) { create(:merge_request, title:"Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } + let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } + before do - bug = create(:label, project: project, title: 'bug') - feature = create(:label, project: project, title: 'feature') - enhancement = create(:label, project: project, title: 'enhancement') + mr1.labels << bug - issue1 = create(:issue, title: "Bugfix1", project: project) - issue1.labels << bug + mr2.labels << bug + mr2.labels << enhancement - issue2 = create(:issue, title: "Bugfix2", project: project) - issue2.labels << bug - issue2.labels << enhancement - - issue3 = create(:issue, title: "Feature1", project: project) - issue3.labels << feature + mr3.title = "Feature1" + mr3.labels << feature project.team << [user, :master] login_as(user) - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) end context 'filter by label bug' do From 1623259ce70f39feca0db0924d65868fb7c773ff Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 14:53:12 -0600 Subject: [PATCH 024/185] Remove spec since it already exists in MR page --- .../issues/filter_by_milestone_spec.rb | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 spec/features/issues/filter_by_milestone_spec.rb diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb deleted file mode 100644 index 9dfa5d1de19..00000000000 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'rails_helper' - -feature 'Issue filtering by Milestone', feature: true do - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } - - scenario 'filters by no Milestone', js: true do - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::None.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone') - expect(page).to have_css('.issue', count: 1) - end - - context 'filters by upcoming milestone', js: true do - it 'does not show issues with no expiry' do - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 0) - end - - it 'shows issues in future' do - milestone = create(:milestone, project: project, due_date: Date.tomorrow) - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 1) - end - - it 'does not show issues in past' do - milestone = create(:milestone, project: project, due_date: Date.yesterday) - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 0) - end - end - - scenario 'filters by a specific Milestone', js: true do - create(:issue, project: project, milestone: milestone) - create(:issue, project: project) - - visit_issues(project) - filter_by_milestone(milestone.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) - expect(page).to have_css('.issue', count: 1) - end - - context 'when milestone has single quotes in title' do - background do - milestone.update(name: "rock 'n' roll") - end - - scenario 'filters by a specific Milestone', js: true do - create(:issue, project: project, milestone: milestone) - create(:issue, project: project) - - visit_issues(project) - filter_by_milestone(milestone.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) - expect(page).to have_css('.issue', count: 1) - end - end - - def visit_issues(project) - visit namespace_project_issues_path(project.namespace, project) - end - - def filter_by_milestone(title) - find(".js-milestone-select").click - find(".milestone-filter .dropdown-content a", text: title).click - end -end From e058ce9f1b98cb7e2ba4873c78eee79ed191806c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 15:10:13 -0600 Subject: [PATCH 025/185] Convert and move reset filters spec to MR --- spec/features/issues/reset_filters_spec.rb | 89 ----------------- .../merge_requests/reset_filters_spec.rb | 96 +++++++++++++++++++ 2 files changed, 96 insertions(+), 89 deletions(-) delete mode 100644 spec/features/issues/reset_filters_spec.rb create mode 100644 spec/features/merge_requests/reset_filters_spec.rb diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb deleted file mode 100644 index c9a3ecf16ea..00000000000 --- a/spec/features/issues/reset_filters_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'rails_helper' - -feature 'Issues filter reset button', feature: true, js: true do - include WaitForAjax - include IssueHelpers - - let!(:project) { create(:project, :public) } - let!(:user) { create(:user)} - let!(:milestone) { create(:milestone, project: project) } - let!(:bug) { create(:label, project: project, name: 'bug')} - let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')} - let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')} - - before do - project.team << [user, :developer] - end - - context 'when a milestone filter has been applied' do - it 'resets the milestone filter' do - visit_issues(project, milestone_title: milestone.title) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when a label filter has been applied' do - it 'resets the label filter' do - visit_issues(project, label_name: bug.name) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when a text search has been conducted' do - it 'resets the text search filter' do - visit_issues(project, search: 'Bug') - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when author filter has been applied' do - it 'resets the author filter' do - visit_issues(project, author_id: user.id) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when assignee filter has been applied' do - it 'resets the assignee filter' do - visit_issues(project, assignee_id: user.id) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when all filters have been applied' do - it 'resets all filters' do - visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') - expect(page).to have_css('.issue', count: 0) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when no filters have been applied' do - it 'the reset link should not be visible' do - visit_issues(project) - expect(page).to have_css('.issue', count: 2) - expect(page).not_to have_css '.reset_filters' - end - end - - def reset_filters - find('.reset-filters').click - end -end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb new file mode 100644 index 00000000000..2e468f2edf3 --- /dev/null +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +feature 'Issues filter reset button', feature: true, js: true do + include WaitForAjax + include IssueHelpers + + let!(:project) { create(:project, :public) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:bug) { create(:label, project: project, name: 'bug')} + let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } + let!(:mr2) { create(:merge_request, title:"Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } + + let(:merge_request_css) { '.merge-request' } + + before do + mr2.labels << bug + project.team << [user, :developer] + end + + context 'when a milestone filter has been applied' do + it 'resets the milestone filter' do + visit_merge_requests(project, milestone_title: milestone.title) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when a label filter has been applied' do + it 'resets the label filter' do + visit_merge_requests(project, label_name: bug.name) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when a text search has been conducted' do + it 'resets the text search filter' do + visit_merge_requests(project, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when author filter has been applied' do + it 'resets the author filter' do + visit_merge_requests(project, author_id: user.id) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when assignee filter has been applied' do + it 'resets the assignee filter' do + visit_merge_requests(project, assignee_id: user.id) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when all filters have been applied' do + it 'resets all filters' do + visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 0) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when no filters have been applied' do + it 'the reset link should not be visible' do + visit_merge_requests(project) + expect(page).to have_css(merge_request_css, count: 2) + expect(page).not_to have_css '.reset_filters' + end + end + + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path project.namespace, project, opts + end + + def reset_filters + find('.reset-filters').click + end +end From e4c233224552484544b764a35a4686a6be3c9dea Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 11 Nov 2016 15:42:40 -0600 Subject: [PATCH 026/185] Add filter by merge request spec based on previous filter by issues spec --- .../filter_merge_requests_spec.rb | 355 ++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 spec/features/merge_requests/filter_merge_requests_spec.rb diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb new file mode 100644 index 00000000000..4642b5a530d --- /dev/null +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -0,0 +1,355 @@ +require 'rails_helper' + +describe 'Filter merge requests', feature: true do + include WaitForAjax + + let!(:project) { create(:project) } + let!(:group) { create(:group) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + + before do + project.team << [user, :master] + group.add_developer(user) + login_as(user) + create(:merge_request, source_project: project, target_project: project) + end + + describe 'for assignee from mr#index' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + + find('.js-assignee-search').click + + find('.dropdown-menu-user-link', text: user.username).click + + wait_for_ajax + end + + context 'assignee', js: true do + it 'updates to current user' do + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'does not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + + it 'does not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + end + end + end + + describe 'for milestone from mr#index' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + + find('.js-milestone-select').click + + find('.milestone-filter .dropdown-content a', text: milestone.title).click + + wait_for_ajax + end + + context 'milestone', js: true do + it 'updates to current milestone' do + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + + it 'does not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + + it 'does not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title) + end + end + end + + describe 'for label from mr#index', js: true do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + find('.js-label-select').click + wait_for_ajax + end + + it 'filters by any label' do + find('.dropdown-menu-labels a', text: 'Any Label').click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + + expect(find('.labels-filter')).to have_content 'Label' + end + + it 'filters by no label' do + find('.dropdown-menu-labels a', text: 'No Label').click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + wait_for_ajax + + page.within '.labels-filter' do + expect(page).to have_content 'Labels' + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels') + end + + it 'filters by a label' do + find('.dropdown-menu-labels a', text: label.title).click + page.within '.labels-filter' do + expect(page).to have_content label.title + end + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + it "filters by `won't fix` and another label" do + page.within '.labels-filter' do + click_link wontfix.title + expect(page).to have_content wontfix.title + click_link label.title + end + + expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more") + end + + it "filters by `won't fix` label followed by another label after page load" do + page.within '.labels-filter' do + click_link wontfix.title + expect(page).to have_content wontfix.title + end + + find('body').click + + expect(find('.filtered-labels')).to have_content(wontfix.title) + + find('.js-label-select').click + wait_for_ajax + find('.dropdown-menu-labels a', text: label.title).click + + find('body').click + + expect(find('.filtered-labels')).to have_content(wontfix.title) + expect(find('.filtered-labels')).to have_content(label.title) + + find('.js-label-select').click + wait_for_ajax + + expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active') + expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active') + end + + it "selects and unselects `won't fix`" do + find('.dropdown-menu-labels a', text: wontfix.title).click + find('.dropdown-menu-labels a', text: wontfix.title).click + # Close label dropdown to load + find('body').click + expect(page).not_to have_css('.filtered-labels') + end + end + + describe 'for assignee and label from issues#index' do + before do + visit namespace_project_merge_requests_path(project.namespace, project) + + find('.js-assignee-search').click + + find('.dropdown-menu-user-link', text: user.username).click + + expect(page).not_to have_selector('.mr-list .merge-request') + + find('.js-label-select').click + + find('.dropdown-menu-labels .dropdown-content a', text: label.title).click + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + + wait_for_ajax + end + + context 'assignee and label', js: true do + it 'updates to current assignee and label' do + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + it 'does not change when closed link is clicked' do + find('.issues-state-filters a', text: "Closed").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + + it 'does not change when all link is clicked' do + find('.issues-state-filters a', text: "All").click + + expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title) + end + end + end + + describe 'filter merge requests by text' do + before do + create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug") + + bug_label = create(:label, project: project, title: 'bug') + milestone = create(:milestone, title: "8", project: project) + + mr = create(:merge_request, + title: "Bug 2", + source_project: project, + target_project: project, + source_branch: "bug2", + milestone: milestone, + author: user, + assignee: user) + mr.labels << bug_label + + visit namespace_project_merge_requests_path(project.namespace, project) + end + + context 'only text', js: true do + it 'filters merge requests by searched text' do + fill_in 'issuable_search', with: 'Bug' + + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + end + + it 'does not show any merge requests' do + fill_in 'issuable_search', with: 'testing' + + page.within '.mr-list' do + expect(page).not_to have_selector('.merge-request') + end + end + end + + context 'text and dropdown options', js: true do + it 'filters by text and label' do + fill_in 'issuable_search', with: 'Bug' + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Label' + page.within '.labels-filter' do + click_link 'bug' + end + find('.dropdown-menu-close-icon').click + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) + end + end + + it 'filters by text and milestone' do + fill_in 'issuable_search', with: 'Bug' + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Milestone' + page.within '.milestone-filter' do + click_link '8' + end + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) + end + end + + it 'filters by text and assignee' do + fill_in 'issuable_search', with: 'Bug' + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Assignee' + page.within '.dropdown-menu-assignee' do + click_link user.name + end + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) + end + end + + it 'filters by text and author' do + fill_in 'issuable_search', with: 'Bug' + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Author' + page.within '.dropdown-menu-author' do + click_link user.name + end + + expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) + end + end + end + end + + describe 'filter merge requests and sort', js: true do + before do + bug_label = create(:label, project: project, title: 'bug') + + mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend") + mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2") + + mr1.labels << bug_label + mr2.labels << bug_label + + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it 'is able to filter and sort merge requests' do + click_button 'Label' + wait_for_ajax + page.within '.labels-filter' do + click_link 'bug' + end + find('.dropdown-menu-close-icon').click + wait_for_ajax + + expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) + end + + click_button 'Last created' + page.within '.dropdown-menu-sort' do + click_link 'Oldest created' + end + wait_for_ajax + + page.within '.mr-list' do + expect(page).to have_content('Frontend') + end + end + end +end From 7f4609198d2a46e8b850a535cb17a3f67d87e25e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 10:25:32 -0600 Subject: [PATCH 027/185] Added more specs --- spec/features/issues/filter_issues_spec.rb | 72 +++++++++++++++++++ .../merge_requests/filter_by_labels_spec.rb | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 7d681742045..2f8e7adad89 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -15,6 +15,7 @@ describe 'Filter issues', feature: true do let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } let!(:milestone) { create(:milestone, title: "8", project: project) } + let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } def input_filtered_search(search_term) filtered_search = find('.filtered-search') @@ -78,6 +79,9 @@ describe 'Filter issues', feature: true do issue_with_everything.labels << bug_label issue_with_everything.labels << caps_sensitive_label + multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project) + multiple_words_label_issue.labels << multiple_words_label + visit namespace_project_issues_path(project.namespace, project) end @@ -190,6 +194,61 @@ describe 'Filter issues', feature: true do input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title}") expect_issues_list_count(1) end + + it 'filters issues by label containing special characters' do + special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') + special_issue = create(:issue, title: "Issue with special character label", project: project) + special_issue.labels << special_label + input_filtered_search("label:#{special_label.title}") + expect_issues_list_count(1) + end + + it 'does not show issues' do + new_label = create(:label, project: project, title: "new_label") + input_filtered_search("label:#{new_label.title}") + expect_no_issues_list() + end + end + + context 'label with multiple words', js: true do + it 'special characters' do + special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce") + special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) + special_multiple_issue.labels << special_multiple_label + + input_filtered_search("label:'#{special_multiple_label.title}'") + expect_issues_list_count(1) + end + + it 'single quotes' do + input_filtered_search("label:'#{multiple_words_label.title}'") + expect_issues_list_count(1) + end + + it 'double quotes' do + input_filtered_search("label:\"#{multiple_words_label.title}\"") + expect_issues_list_count(1) + end + + it 'single quotes containing double quotes' do + # TODO: Actual bug + + # double_quotes_label = create(:label, project: project, title: 'won"t fix') + # double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) + # double_quotes_label_issue.labels << double_quotes_label + + # input_filtered_search("label:'#{double_quotes_label.title}'") + # expect_issues_list_count(1) + end + + it 'double quotes containing single quotes' do + single_quotes_label = create(:label, project: project, title: "won't fix") + single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) + single_quotes_label_issue.labels << single_quotes_label + + input_filtered_search("label:\"#{single_quotes_label.title}\"") + expect_issues_list_count(1) + end end context 'label with other filters', js: true do @@ -269,6 +328,19 @@ describe 'Filter issues', feature: true do it 'filters issues by multiple milestones' do # YOLO end + + it 'filters issues by milestone containing special characters' do + special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) + create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) + input_filtered_search('milestone:!@\#{$%^&*()}') + expect_issues_list_count(1) + end + + it 'does not show issues' do + new_milestone = create(:milestone, title: "new", project: project) + input_filtered_search("milestone:#{new_milestone}") + expect_no_issues_list() + end end context 'milestone with other filters', js: true do diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb index eff350ed53a..4c60329865c 100644 --- a/spec/features/merge_requests/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -12,7 +12,7 @@ feature 'Issue filtering by Labels', feature: true, js: true do let!(:enhancement) { create(:label, project: project, title: 'enhancement') } let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } - let!(:mr2) { create(:merge_request, title:"Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } + let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } before do From 01eb0571f0498225c3d75df419b8a50a47739dc8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 10:37:55 -0600 Subject: [PATCH 028/185] Resolve MR review suggestions --- .../filtered_search/filtered_search_bundle.js | 6 ---- .../filtered_search_manager.js.es6 | 28 +++++++++---------- .../filtered_search_tokenizer.es6 | 2 +- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index 656979ba82f..d188718c5f3 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -1,4 +1,3 @@ - /* eslint-disable */ // This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js @@ -6,8 +5,3 @@ // the compiled file. // /*= require_tree . */ - - (function() { - - }).call(this); - \ No newline at end of file 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 db414b9755d..26b9d334545 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -18,22 +18,22 @@ param: 'name[]', }]; - function clearSearch(event) { - event.stopPropagation(); - event.preventDefault(); + function clearSearch(e) { + e.stopPropagation(); + e.preventDefault(); document.querySelector('.filtered-search').value = ''; document.querySelector('.clear-search').classList.add('hidden'); } - function toggleClearSearchButton(event) { + function toggleClearSearchButton(e) { const clearSearchButton = document.querySelector('.clear-search'); if (event.target.value) { - clearSearchButton.classList.remove('hidden'); - } else { - clearSearchButton.classList.add('hidden'); - } + clearSearchButton.classList.remove('hidden'); + } else { + clearSearchButton.classList.add('hidden'); + } } function loadSearchParamsFromURL() { @@ -97,16 +97,16 @@ document.querySelector('.clear-search').addEventListener('click', clearSearch); } - processInput(event) { - const input = event.target.value; + processInput(e) { + const input = e.target.value; this.tokenizer.processTokens(input); } - checkForEnter(event) { + checkForEnter(e) { // Enter KeyCode - if (event.keyCode === 13) { - event.stopPropagation(); - event.preventDefault(); + if (e.keyCode === 13) { + e.stopPropagation(); + e.preventDefault(); this.search(); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index ddb173b2d98..de91081edfa 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -36,7 +36,7 @@ inputs.forEach((i) => { if (incompleteToken) { - const prevToken = this.tokens[this.tokens.length - 1]; + const prevToken = this.tokens.last(); prevToken.value += ` ${i}`; // Remove last quotation From 329b03b3c3fa51f365dee867cf4d8cef5ad23d4e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 13:22:32 -0600 Subject: [PATCH 029/185] Add token symbol matching --- .../filtered_search_manager.js.es6 | 92 ++++++++++++++----- .../filtered_search_tokenizer.es6 | 10 +- 2 files changed, 77 insertions(+), 25 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 26b9d334545..31e570bd6b6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -4,18 +4,37 @@ key: 'author', type: 'string', param: 'username', + symbol: '@', }, { key: 'assignee', type: 'string', param: 'username', + symbol: '@', + conditions: [{ + keyword: 'none', + url: 'assignee_id=0', + }] }, { key: 'milestone', type: 'string', param: 'title', + symbol: '%', + conditions: [{ + keyword: 'none', + url: 'milestone_title=No+Milestone', + }, { + keyword: 'upcoming', + url: 'milestone_title=%23upcoming', + }] }, { key: 'label', type: 'array', param: 'name[]', + symbol: '~', + conditions: [{ + keyword: 'none', + url: 'label_name[]=No+Label', + }] }]; function clearSearch(e) { @@ -47,28 +66,42 @@ const key = decodeURIComponent(split[0]); const value = split[1]; - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; - const match = validTokenKeys.filter(t => key === `${t.key}_${t.param}`)[0]; - - if (match) { - const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; - - const preferredQuotations = '"'; - let quotationsToUse = preferredQuotations; - - if (valueHasSpace) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + // Check if it matches edge conditions listed in validTokenKeys + let conditionIndex = 0; + const validCondition = validTokenKeys.filter(v => v.conditions && v.conditions.filter((c, index) => { + if (c.url === p) { + conditionIndex = index; } + return c.url === p; + })[0])[0]; - inputValue += valueHasSpace ? `${sanitizedKey}:${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${sanitizedValue}`; - inputValue += ' '; - } else if (!match && key === 'search') { - inputValue += sanitizedValue; - inputValue += ' '; + if (validCondition) { + inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; + const match = validTokenKeys.filter(t => key === `${t.key}_${t.param}`)[0]; + + if (match) { + const sanitizedKey = key.slice(0, key.indexOf('_')); + const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; + const symbol = match.symbol; + + const preferredQuotations = '"'; + let quotationsToUse = preferredQuotations; + + if (valueHasSpace) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + } + + inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; + inputValue += ' '; + } else if (!match && key === 'search') { + inputValue += sanitizedValue; + inputValue += ' '; + } } }); @@ -133,8 +166,23 @@ path += `&state=${currentState}`; tokens.forEach((token) => { - const param = validTokenKeys.filter(t => t.key === token.key)[0].param; - path += `&${token.key}_${param}=${encodeURIComponent(token.value)}`; + const match = validTokenKeys.filter(t => t.key === token.key)[0]; + let tokenPath = ''; + + if (token.wildcard && match.conditions) { + const condition = match.conditions.filter(c => c.keyword === token.value.toLowerCase())[0]; + + if (condition) { + tokenPath = `${condition.url}`; + } + } else if (!token.wildcard) { + // Remove the wildcard token + tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value.slice(1))}`; + } else { + tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value)}`; + } + + path += `&${tokenPath}`; }); if (searchToken) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index de91081edfa..c3e5e817c9e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -57,7 +57,10 @@ if (colonIndex !== -1) { const tokenKey = i.slice(0, colonIndex).toLowerCase(); const tokenValue = i.slice(colonIndex + 1); - const match = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; + const tokenSymbol = tokenValue[0]; + console.log(tokenSymbol) + const keyMatch = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; + const symbolMatch = this.validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; if (tokenValue.indexOf('"') !== -1) { lastQuotation = '"'; @@ -67,10 +70,11 @@ incompleteToken = true; } - if (match && tokenValue.length > 0) { + if (keyMatch && tokenValue.length > 0) { this.tokens.push({ - key: match.key, + key: keyMatch.key, value: tokenValue, + wildcard: symbolMatch ? false : true, }); return; From c07a522743443cbf7afe1ce85d014a1b9f41e8d8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 14:43:09 -0600 Subject: [PATCH 030/185] Update tests to include token symbol --- spec/features/issues/filter_issues_spec.rb | 178 +++++++++++---------- 1 file changed, 95 insertions(+), 83 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 2f8e7adad89..c790f350b2d 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -82,13 +82,19 @@ describe 'Filter issues', feature: true do multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project) multiple_words_label_issue.labels << multiple_words_label + future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month) + issue_with_future_milestone = create(:issue, + title: "Issue with future milestone", + milestone: future_milestone, + project: project) + visit namespace_project_issues_path(project.namespace, project) end describe 'filter issues by author' do context 'only author', js: true do it 'filters issues by searched author' do - input_filtered_search("author:#{user.username}") + input_filtered_search("author:@#{user.username}") expect_issues_list_count(5) end @@ -103,22 +109,22 @@ describe 'Filter issues', feature: true do context 'author with other filters', js: true do it 'filters issues by searched author and text' do - input_filtered_search("author:#{user.username} issue") + input_filtered_search("author:@#{user.username} issue") expect_issues_list_count(3) end it 'filters issues by searched author, assignee and text' do - input_filtered_search("author:#{user.username} assignee:#{user.username} issue") + input_filtered_search("author:@#{user.username} assignee:@#{user.username} issue") expect_issues_list_count(3) end it 'filters issues by searched author, assignee, label, and text' do - input_filtered_search("author:#{user.username} assignee:#{user.username} label:#{caps_sensitive_label.title} issue") + input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue") expect_issues_list_count(1) end it 'filters issues by searched author, assignee, label, milestone and text' do - input_filtered_search("author:#{user.username} assignee:#{user.username} label:#{caps_sensitive_label.title} milestone:#{milestone.title} issue") + input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue") expect_issues_list_count(1) end end @@ -131,12 +137,13 @@ describe 'Filter issues', feature: true do describe 'filter issues by assignee' do context 'only assignee', js: true do it 'filters issues by searched assignee' do - input_filtered_search("assignee:#{user.username}") + input_filtered_search("assignee:@#{user.username}") expect_issues_list_count(5) end it 'filters issues by no assignee' do - # TODO + input_filtered_search("assignee:none") + expect_issues_list_count(8) end it 'filters issues by invalid assignee' do @@ -148,27 +155,27 @@ describe 'Filter issues', feature: true do end end - context 'assignee with other filters', js: true do - it 'filters issues by searched assignee and text' do - input_filtered_search("assignee:#{user.username} searchTerm") - expect_issues_list_count(2) - end + # context 'assignee with other filters', js: true do + # it 'filters issues by searched assignee and text' do + # input_filtered_search("assignee:@#{user.username} searchTerm") + # expect_issues_list_count(2) + # end - it 'filters issues by searched assignee, author and text' do - input_filtered_search("assignee:#{user.username} author:#{user.username} searchTerm") - expect_issues_list_count(2) - end + # it 'filters issues by searched assignee, author and text' do + # input_filtered_search("assignee:@#{user.username} author:@#{user.username} searchTerm") + # expect_issues_list_count(2) + # end - it 'filters issues by searched assignee, author, label, text' do - input_filtered_search("assignee:#{user.username} author:#{user.username} label:#{caps_sensitive_label.title} searchTerm") - expect_issues_list_count(1) - end + # it 'filters issues by searched assignee, author, label, text' do + # input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm") + # expect_issues_list_count(1) + # end - it 'filters issues by searched assignee, author, label, milestone and text' do - input_filtered_search("assignee:#{user.username} author:#{user.username} label:#{caps_sensitive_label.title} milestone:#{milestone.title} searchTerm") - expect_issues_list_count(1) - end - end + # it 'filters issues by searched assignee, author, label, milestone and text' do + # input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm") + # expect_issues_list_count(1) + # end + # end context 'sorting', js: true do # TODO @@ -178,12 +185,13 @@ describe 'Filter issues', feature: true do describe 'filter issues by label' do context 'only label', js: true do it 'filters issues by searched label' do - input_filtered_search("label:#{bug_label.title}") + input_filtered_search("label:~#{bug_label.title}") expect_issues_list_count(2) end it 'filters issues by no label' do - # TODO + input_filtered_search("label:none") + expect_issues_list_count(9) end it 'filters issues by invalid label' do @@ -191,7 +199,7 @@ describe 'Filter issues', feature: true do end it 'filters issues by multiple labels' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title}") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}") expect_issues_list_count(1) end @@ -199,13 +207,13 @@ describe 'Filter issues', feature: true do special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') special_issue = create(:issue, title: "Issue with special character label", project: project) special_issue.labels << special_label - input_filtered_search("label:#{special_label.title}") + input_filtered_search("label:~#{special_label.title}") expect_issues_list_count(1) end it 'does not show issues' do new_label = create(:label, project: project, title: "new_label") - input_filtered_search("label:#{new_label.title}") + input_filtered_search("label:~#{new_label.title}") expect_no_issues_list() end end @@ -216,17 +224,17 @@ describe 'Filter issues', feature: true do special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) special_multiple_issue.labels << special_multiple_label - input_filtered_search("label:'#{special_multiple_label.title}'") + input_filtered_search("label:~'#{special_multiple_label.title}'") expect_issues_list_count(1) end it 'single quotes' do - input_filtered_search("label:'#{multiple_words_label.title}'") + input_filtered_search("label:~'#{multiple_words_label.title}'") expect_issues_list_count(1) end it 'double quotes' do - input_filtered_search("label:\"#{multiple_words_label.title}\"") + input_filtered_search("label:~\"#{multiple_words_label.title}\"") expect_issues_list_count(1) end @@ -246,51 +254,51 @@ describe 'Filter issues', feature: true do single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) single_quotes_label_issue.labels << single_quotes_label - input_filtered_search("label:\"#{single_quotes_label.title}\"") + input_filtered_search("label:~\"#{single_quotes_label.title}\"") expect_issues_list_count(1) end end context 'label with other filters', js: true do it 'filters issues by searched label and text' do - input_filtered_search("label:#{caps_sensitive_label.title} bug") + input_filtered_search("label:~#{caps_sensitive_label.title} bug") expect_issues_list_count(1) end it 'filters issues by searched label, author and text' do - input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} bug") + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} bug") expect_issues_list_count(1) end it 'filters issues by searched label, author, assignee and text' do - input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} bug") + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug") expect_issues_list_count(1) end it 'filters issues by searched label, author, assignee, milestone and text' do - input_filtered_search("label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} milestone:#{milestone.title} bug") + input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug") expect_issues_list_count(1) end end context 'multiple labels with other filters', js: true do it 'filters issues by searched label, label2, and text' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} bug") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug") expect_issues_list_count(1) end it 'filters issues by searched label, label2, author and text' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} bug") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug") expect_issues_list_count(1) end it 'filters issues by searched label, label2, author, assignee and text' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} bug") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug") expect_issues_list_count(1) end it 'filters issues by searched label, label2, author, assignee, milestone and text' do - input_filtered_search("label:#{bug_label.title} label:#{caps_sensitive_label.title} author:#{user.username} assignee:#{user.username} milestone:#{milestone.title} bug") + input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug") expect_issues_list_count(1) end end @@ -309,16 +317,18 @@ describe 'Filter issues', feature: true do describe 'filter issues by milestone' do context 'only milestone', js: true do it 'filters issues by searched milestone' do - input_filtered_search("milestone:#{milestone.title}") + input_filtered_search("milestone:%#{milestone.title}") expect_issues_list_count(5) end it 'filters issues by no milestone' do - # TODO + input_filtered_search("milestone:none") + expect_issues_list_count(7) end it 'filters issues by upcoming milestones' do - # TODO + input_filtered_search("milestone:upcoming") + expect_issues_list_count(1) end it 'filters issues by invalid milestones' do @@ -332,13 +342,13 @@ describe 'Filter issues', feature: true do it 'filters issues by milestone containing special characters' do special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) - input_filtered_search('milestone:!@\#{$%^&*()}') + input_filtered_search('milestone:%!@\#{$%^&*()}') expect_issues_list_count(1) end it 'does not show issues' do new_milestone = create(:milestone, title: "new", project: project) - input_filtered_search("milestone:#{new_milestone}") + input_filtered_search("milestone:%#{new_milestone}") expect_no_issues_list() end end @@ -402,57 +412,57 @@ describe 'Filter issues', feature: true do context 'searched text with other filters', js: true do it 'filters issues by searched text and author' do - input_filtered_search("bug author:#{user.username}") + input_filtered_search("bug author:@#{user.username}") expect_issues_list_count(2) end it 'filters issues by searched text, author and more text' do - input_filtered_search("bug author:#{user.username} report") + input_filtered_search("bug author:@#{user.username} report") expect_issues_list_count(1) end it 'filters issues by searched text, author and assignee' do - input_filtered_search("bug author:#{user.username} assignee:#{user.username}") + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") expect_issues_list_count(2) end it 'filters issues by searched text, author, more text and assignee' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username}") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") expect_issues_list_count(1) end it 'filters issues by searched text, author, more text, assignee and even more text' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") expect_issues_list_count(1) end it 'filters issues by searched text, author, assignee and label' do - input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title}") + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") expect_issues_list_count(2) end it 'filters issues by searched text, author, text, assignee, text, label and text' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") expect_issues_list_count(1) end it 'filters issues by searched text, author, assignee, label and milestone' do - input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title} milestone:#{milestone.title}") + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") expect_issues_list_count(2) end it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything milestone:#{milestone.title} you") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") expect_issues_list_count(1) end it 'filters issues by searched text, author, assignee, multiple labels and milestone' do - input_filtered_search("bug author:#{user.username} assignee:#{user.username} label:#{bug_label.title} label:#{caps_sensitive_label.title} milestone:#{milestone.title}") + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") expect_issues_list_count(1) end it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do - input_filtered_search("bug author:#{user.username} report assignee:#{user.username} with label:#{bug_label.title} everything label:#{caps_sensitive_label.title} you milestone:#{milestone.title} thought") + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") expect_issues_list_count(1) end end @@ -462,31 +472,33 @@ describe 'Filter issues', feature: true do end end - it 'updates atom feed link for project issues' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) - end + describe 'RSS feeds' do + it 'updates atom feed link for project issues' do + visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI::parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => ['']) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => ['']) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end - it 'updates atom feed link for group issues' do - visit issues_group_path(group, milestone_title: '', assignee_id: user.id) - link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + it 'updates atom feed link for group issues' do + visit issues_group_path(group, milestone_title: '', assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI::parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => ['']) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => ['']) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end end end From e3395e3a9d683f62fb67e4d7b54381aad73e7a9e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 16:55:38 -0600 Subject: [PATCH 031/185] Add specs for clear search button --- spec/features/issues/search_bar_spec.rb | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 spec/features/issues/search_bar_spec.rb diff --git a/spec/features/issues/search_bar_spec.rb b/spec/features/issues/search_bar_spec.rb new file mode 100644 index 00000000000..1d632671fe2 --- /dev/null +++ b/spec/features/issues/search_bar_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +describe 'Search bar', feature: true do + include WaitForAjax + + let!(:project) { create(:project) } + let!(:group) { create(:group) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + group.add_developer(user) + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'clear search button', js: true do + it 'clears text' do + search_text = 'search_text' + filtered_search = find('.filtered-search') + filtered_search.set(search_text) + + expect(filtered_search.value).to eq(search_text) + find('.filtered-search-input-container .clear-search').click + expect(filtered_search.value).to eq('') + end + + it 'hides by default' do + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides after clicked' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + find('.filtered-search-input-container .clear-search').click + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides when there is no text' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + filtered_search.set('') + expect(page).to have_css('.clear-search', visible: false) + end + + it 'shows when there is text' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + + expect(page).to have_css('.clear-search', visible: true) + end + end +end From 8e3a52cfd68302ed75ffb89de3a08d1f70f876ad Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 17:45:26 -0600 Subject: [PATCH 032/185] Fix eslint --- .../filtered_search_manager.js.es6 | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 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 31e570bd6b6..8568bf78416 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -13,7 +13,7 @@ conditions: [{ keyword: 'none', url: 'assignee_id=0', - }] + }], }, { key: 'milestone', type: 'string', @@ -25,7 +25,7 @@ }, { keyword: 'upcoming', url: 'milestone_title=%23upcoming', - }] + }], }, { key: 'label', type: 'array', @@ -34,7 +34,7 @@ conditions: [{ keyword: 'none', url: 'label_name[]=No+Label', - }] + }], }]; function clearSearch(e) { @@ -48,11 +48,11 @@ function toggleClearSearchButton(e) { const clearSearchButton = document.querySelector('.clear-search'); - if (event.target.value) { - clearSearchButton.classList.remove('hidden'); - } else { - clearSearchButton.classList.add('hidden'); - } + if (e.target.value) { + clearSearchButton.classList.remove('hidden'); + } else { + clearSearchButton.classList.add('hidden'); + } } function loadSearchParamsFromURL() { @@ -68,12 +68,13 @@ // Check if it matches edge conditions listed in validTokenKeys let conditionIndex = 0; - const validCondition = validTokenKeys.filter(v => v.conditions && v.conditions.filter((c, index) => { - if (c.url === p) { - conditionIndex = index; - } - return c.url === p; - })[0])[0]; + const validCondition = validTokenKeys + .filter(v => v.conditions && v.conditions.filter((c, index) => { + if (c.url === p) { + conditionIndex = index; + } + return c.url === p; + })[0])[0]; if (validCondition) { inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; @@ -170,7 +171,8 @@ let tokenPath = ''; if (token.wildcard && match.conditions) { - const condition = match.conditions.filter(c => c.keyword === token.value.toLowerCase())[0]; + const condition = match.conditions + .filter(c => c.keyword === token.value.toLowerCase())[0]; if (condition) { tokenPath = `${condition.url}`; From 8be495318e6d477f98dd08de11261996faa050f2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 18:19:40 -0600 Subject: [PATCH 033/185] Add more specs --- spec/features/issues/filter_issues_spec.rb | 106 ++++++++++++++---- .../merge_requests/reset_filters_spec.rb | 2 +- 2 files changed, 83 insertions(+), 25 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index c790f350b2d..0eed0ed4274 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -17,6 +17,8 @@ describe 'Filter issues', feature: true do let!(:milestone) { create(:milestone, title: "8", project: project) } let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } + let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } + def input_filtered_search(search_term) filtered_search = find('.filtered-search') filtered_search.set(search_term) @@ -83,7 +85,8 @@ describe 'Filter issues', feature: true do multiple_words_label_issue.labels << multiple_words_label future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month) - issue_with_future_milestone = create(:issue, + + create(:issue, title: "Issue with future milestone", milestone: future_milestone, project: project) @@ -143,7 +146,7 @@ describe 'Filter issues', feature: true do it 'filters issues by no assignee' do input_filtered_search("assignee:none") - expect_issues_list_count(8) + expect_issues_list_count(8, 1) end it 'filters issues by invalid assignee' do @@ -155,27 +158,27 @@ describe 'Filter issues', feature: true do end end - # context 'assignee with other filters', js: true do - # it 'filters issues by searched assignee and text' do - # input_filtered_search("assignee:@#{user.username} searchTerm") - # expect_issues_list_count(2) - # end + context 'assignee with other filters', js: true do + it 'filters issues by searched assignee and text' do + input_filtered_search("assignee:@#{user.username} searchTerm") + expect_issues_list_count(2) + end - # it 'filters issues by searched assignee, author and text' do - # input_filtered_search("assignee:@#{user.username} author:@#{user.username} searchTerm") - # expect_issues_list_count(2) - # end + it 'filters issues by searched assignee, author and text' do + input_filtered_search("assignee:@#{user.username} author:@#{user.username} searchTerm") + expect_issues_list_count(2) + end - # it 'filters issues by searched assignee, author, label, text' do - # input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm") - # expect_issues_list_count(1) - # end + it 'filters issues by searched assignee, author, label, text' do + input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm") + expect_issues_list_count(1) + end - # it 'filters issues by searched assignee, author, label, milestone and text' do - # input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm") - # expect_issues_list_count(1) - # end - # end + it 'filters issues by searched assignee, author, label, milestone and text' do + input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm") + expect_issues_list_count(1) + end + end context 'sorting', js: true do # TODO @@ -191,7 +194,7 @@ describe 'Filter issues', feature: true do it 'filters issues by no label' do input_filtered_search("label:none") - expect_issues_list_count(9) + expect_issues_list_count(9, 1) end it 'filters issues by invalid label' do @@ -323,7 +326,7 @@ describe 'Filter issues', feature: true do it 'filters issues by no milestone' do input_filtered_search("milestone:none") - expect_issues_list_count(7) + expect_issues_list_count(7, 1) end it 'filters issues by upcoming milestones' do @@ -376,7 +379,7 @@ describe 'Filter issues', feature: true do context 'only text', js: true do it 'filters issues by searched text' do input_filtered_search('Bug') - expect_issues_list_count(4) + expect_issues_list_count(4, 1) end it 'filters issues by multiple searched text' do @@ -468,7 +471,62 @@ describe 'Filter issues', feature: true do end context 'sorting', js: true do - # TODO + it 'sorts by oldest updated' do + create(:issue, + title: '3 days ago', + project: project, + author: user, + created_at: 3.days.ago, + updated_at: 3.days.ago) + + old_issue = create(:issue, + title: '5 days ago', + project: project, + author: user, + created_at: 5.days.ago, + updated_at: 5.days.ago) + + input_filtered_search('days ago') + expect_issues_list_count(2) + + sort_toggle = find('.filtered-search-container .dropdown-toggle') + sort_toggle.click + + find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click + wait_for_ajax + + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) + end + end + end + + describe 'retains filter when switching issue states', js: true do + before do + input_filtered_search('bug') + expect_issues_list_count(4, 1) + end + + it 'open state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + + find('.issues-state-filters a', text: 'Open').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 4) + end + + it 'closed state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 1) + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) + end + + it 'all state' do + find('.issues-state-filters a', text: 'All').click + wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 5) end end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb index 2e468f2edf3..3a7ece7e1d6 100644 --- a/spec/features/merge_requests/reset_filters_spec.rb +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -9,7 +9,7 @@ feature 'Issues filter reset button', feature: true, js: true do let!(:milestone) { create(:milestone, project: project) } let!(:bug) { create(:label, project: project, name: 'bug')} let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } - let!(:mr2) { create(:merge_request, title:"Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } + let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } let(:merge_request_css) { '.merge-request' } From 976893ec2fa1e4289f5d923a41d296e170bdf3af Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 14 Nov 2016 22:14:29 -0600 Subject: [PATCH 034/185] Add support for labels containing single/double quote --- .../filtered_search/filtered_search_tokenizer.es6 | 14 ++++++++++++-- spec/features/issues/filter_issues_spec.rb | 12 +++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index c3e5e817c9e..eab805c4714 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -62,10 +62,20 @@ const keyMatch = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; const symbolMatch = this.validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; - if (tokenValue.indexOf('"') !== -1) { + const doubleQuoteIndex = tokenValue.indexOf('"'); + const singleQuoteIndex = tokenValue.indexOf('\''); + + const doubleQuoteExist = doubleQuoteIndex !== -1; + const singleQuoteExist = singleQuoteIndex !== -1; + + if ((doubleQuoteExist && !singleQuoteExist) || + (doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex)) { + // " is found and is in front of ' (if any) lastQuotation = '"'; incompleteToken = true; - } else if (tokenValue.indexOf('\'') !== -1) { + } else if ((singleQuoteExist && !doubleQuoteExist) || + (doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex)) { + // ' is found and is in front of " (if any) lastQuotation = '\''; incompleteToken = true; } diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 0eed0ed4274..ba9f7579627 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -242,14 +242,12 @@ describe 'Filter issues', feature: true do end it 'single quotes containing double quotes' do - # TODO: Actual bug + double_quotes_label = create(:label, project: project, title: 'won"t fix') + double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) + double_quotes_label_issue.labels << double_quotes_label - # double_quotes_label = create(:label, project: project, title: 'won"t fix') - # double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) - # double_quotes_label_issue.labels << double_quotes_label - - # input_filtered_search("label:'#{double_quotes_label.title}'") - # expect_issues_list_count(1) + input_filtered_search("label:~'#{double_quotes_label.title}'") + expect_issues_list_count(1) end it 'double quotes containing single quotes' do From b50b916b8c4f6db2fd5aa3f6536ec493c9f4cb0b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 15 Nov 2016 12:28:48 -0600 Subject: [PATCH 035/185] Fix failing spec --- spec/features/issues/filter_issues_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index ba9f7579627..608e6f20748 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -501,7 +501,9 @@ describe 'Filter issues', feature: true do describe 'retains filter when switching issue states', js: true do before do input_filtered_search('bug') - expect_issues_list_count(4, 1) + + # Wait for search results to load + sleep 1 end it 'open state' do From 3d67042275f61f515cc4c3312944e337563fc658 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 15 Nov 2016 12:55:37 -0600 Subject: [PATCH 036/185] Add spec for issue label clicked --- spec/features/issues/filter_issues_spec.rb | 210 ++++++++++++++++----- 1 file changed, 165 insertions(+), 45 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 608e6f20748..cbb11b790ec 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -3,9 +3,8 @@ require 'rails_helper' describe 'Filter issues', feature: true do include WaitForAjax - let!(:group) { create(:group) } let!(:project) { create(:project) } - let!(:user) { create(:user)} + let!(:group) { create(:group) } let!(:user) { create(:user) } let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } @@ -25,6 +24,10 @@ describe 'Filter issues', feature: true do filtered_search.send_keys(:enter) end + def expect_filtered_search_input(input) + expect(find('.filtered-search').value).to eq(input) + end + def expect_no_issues_list page.within '.issues-list' do expect(page).not_to have_selector('.issue') @@ -112,23 +115,31 @@ describe 'Filter issues', feature: true do context 'author with other filters', js: true do it 'filters issues by searched author and text' do - input_filtered_search("author:@#{user.username} issue") + search = "author:@#{user.username} issue" + input_filtered_search(search) expect_issues_list_count(3) + expect_filtered_search_input(search) end it 'filters issues by searched author, assignee and text' do - input_filtered_search("author:@#{user.username} assignee:@#{user.username} issue") + search = "author:@#{user.username} assignee:@#{user.username} issue" + input_filtered_search(search) expect_issues_list_count(3) + expect_filtered_search_input(search) end it 'filters issues by searched author, assignee, label, and text' do - input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue") + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched author, assignee, label, milestone and text' do - input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue") + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end @@ -140,13 +151,17 @@ describe 'Filter issues', feature: true do describe 'filter issues by assignee' do context 'only assignee', js: true do it 'filters issues by searched assignee' do - input_filtered_search("assignee:@#{user.username}") + search = "assignee:@#{user.username}" + input_filtered_search(search) expect_issues_list_count(5) + expect_filtered_search_input(search) end it 'filters issues by no assignee' do - input_filtered_search("assignee:none") + search = "assignee:none" + input_filtered_search(search) expect_issues_list_count(8, 1) + expect_filtered_search_input(search) end it 'filters issues by invalid assignee' do @@ -160,23 +175,31 @@ describe 'Filter issues', feature: true do context 'assignee with other filters', js: true do it 'filters issues by searched assignee and text' do - input_filtered_search("assignee:@#{user.username} searchTerm") + search = "assignee:@#{user.username} searchTerm" + input_filtered_search(search) expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched assignee, author and text' do - input_filtered_search("assignee:@#{user.username} author:@#{user.username} searchTerm") + search = "assignee:@#{user.username} author:@#{user.username} searchTerm" + input_filtered_search(search) expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched assignee, author, label, text' do - input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm") + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched assignee, author, label, milestone and text' do - input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm") + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end @@ -188,13 +211,17 @@ describe 'Filter issues', feature: true do describe 'filter issues by label' do context 'only label', js: true do it 'filters issues by searched label' do - input_filtered_search("label:~#{bug_label.title}") + search = "label:~#{bug_label.title}" + input_filtered_search(search) expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by no label' do - input_filtered_search("label:none") + search = "label:none" + input_filtered_search(search) expect_issues_list_count(9, 1) + expect_filtered_search_input(search) end it 'filters issues by invalid label' do @@ -202,22 +229,30 @@ describe 'Filter issues', feature: true do end it 'filters issues by multiple labels' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by label containing special characters' do special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') special_issue = create(:issue, title: "Issue with special character label", project: project) special_issue.labels << special_label - input_filtered_search("label:~#{special_label.title}") + + search = "label:~#{special_label.title}" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'does not show issues' do new_label = create(:label, project: project, title: "new_label") - input_filtered_search("label:~#{new_label.title}") + + search = "label:~#{new_label.title}" + input_filtered_search(search) expect_no_issues_list() + expect_filtered_search_input(search) end end @@ -227,18 +262,27 @@ describe 'Filter issues', feature: true do special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) special_multiple_issue.labels << special_multiple_label - input_filtered_search("label:~'#{special_multiple_label.title}'") + search = "label:~'#{special_multiple_label.title}'" + input_filtered_search(search) expect_issues_list_count(1) + + # filtered search defaults quotations to double quotes + expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"") end it 'single quotes' do - input_filtered_search("label:~'#{multiple_words_label.title}'") + search = "label:~'#{multiple_words_label.title}'" + input_filtered_search(search) expect_issues_list_count(1) + + expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") end it 'double quotes' do - input_filtered_search("label:~\"#{multiple_words_label.title}\"") + search = "label:~\"#{multiple_words_label.title}\"" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'single quotes containing double quotes' do @@ -246,8 +290,10 @@ describe 'Filter issues', feature: true do double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) double_quotes_label_issue.labels << double_quotes_label - input_filtered_search("label:~'#{double_quotes_label.title}'") + search = "label:~'#{double_quotes_label.title}'" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'double quotes containing single quotes' do @@ -255,61 +301,88 @@ describe 'Filter issues', feature: true do single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) single_quotes_label_issue.labels << single_quotes_label - input_filtered_search("label:~\"#{single_quotes_label.title}\"") + search = "label:~\"#{single_quotes_label.title}\"" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end context 'label with other filters', js: true do it 'filters issues by searched label and text' do - input_filtered_search("label:~#{caps_sensitive_label.title} bug") + search = "label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, author and text' do - input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} bug") + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, author, assignee and text' do - input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug") + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, author, assignee, milestone and text' do - input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug") + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end context 'multiple labels with other filters', js: true do it 'filters issues by searched label, label2, and text' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, label2, author and text' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, label2, author, assignee and text' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched label, label2, author, assignee, milestone and text' do - input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug") + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end end - it "selects and unselects `won't fix`" do - find('.dropdown-menu-labels a', text: wontfix.title).click - find('.dropdown-menu-labels a', text: wontfix.title).click + context 'issue label clicked', js: true do + before do + find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click + sleep 1 + end + + it 'filters' do + expect_issues_list_count(1) + end + + it 'displays in search bar' do + expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") + end + end - find('.dropdown-menu-close-icon').click - expect(page).not_to have_css('.filtered-labels') context 'sorting', js: true do # TODO end @@ -343,28 +416,50 @@ describe 'Filter issues', feature: true do it 'filters issues by milestone containing special characters' do special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) - input_filtered_search('milestone:%!@\#{$%^&*()}') + + search = "milestone:%#{special_milestone.title}" + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'does not show issues' do new_milestone = create(:milestone, title: "new", project: project) - input_filtered_search("milestone:%#{new_milestone}") + + search = "milestone:%#{new_milestone.title}" + input_filtered_search(search) expect_no_issues_list() + expect_filtered_search_input(search) end end context 'milestone with other filters', js: true do it 'filters issues by searched milestone and text' do + search = "milestone:%#{milestone.title} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched milestone, author and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched milestone, author, assignee and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) end it 'filters issues by searched milestone, author, assignee, label and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" + input_filtered_search(search) + expect_issues_list_count(2) + expect_filtered_search_input(search) end end @@ -376,38 +471,52 @@ describe 'Filter issues', feature: true do describe 'filter issues by text' do context 'only text', js: true do it 'filters issues by searched text' do - input_filtered_search('Bug') + search = 'Bug' + input_filtered_search(search) expect_issues_list_count(4, 1) + expect_filtered_search_input(search) end it 'filters issues by multiple searched text' do - input_filtered_search('Bug report') + search = 'Bug report' + input_filtered_search(search) expect_issues_list_count(3) + expect_filtered_search_input(search) end it 'filters issues by case insensitive searched text' do - input_filtered_search('bug report') + search = 'bug report' + input_filtered_search(search) expect_issues_list_count(3) + expect_filtered_search_input(search) end it 'filters issues by searched text containing single quotes' do - input_filtered_search('\'single quotes\'') + search = '\'single quotes\'' + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched text containing double quotes' do - input_filtered_search('"double quotes"') + search = '"double quotes"' + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'filters issues by searched text containing special characters' do - input_filtered_search('!@#{$%^&*()-+') + search = '!@#{$%^&*()-+' + input_filtered_search(search) expect_issues_list_count(1) + expect_filtered_search_input(search) end it 'does not show any issues' do - input_filtered_search('testing') + search = 'testing' + input_filtered_search(search) expect_no_issues_list() + expect_filtered_search_input(search) end end @@ -415,56 +524,67 @@ describe 'Filter issues', feature: true do it 'filters issues by searched text and author' do input_filtered_search("bug author:@#{user.username}") expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} bug") end it 'filters issues by searched text, author and more text' do input_filtered_search("bug author:@#{user.username} report") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} bug report") end it 'filters issues by searched text, author and assignee' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") end it 'filters issues by searched text, author, more text and assignee' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") end it 'filters issues by searched text, author, more text, assignee and even more text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") end it 'filters issues by searched text, author, assignee and label' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") end it 'filters issues by searched text, author, assignee, label and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") end it 'filters issues by searched text, author, assignee, multiple labels and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") end end @@ -503,7 +623,7 @@ describe 'Filter issues', feature: true do input_filtered_search('bug') # Wait for search results to load - sleep 1 + sleep 2 end it 'open state' do From 8ecc2117db3a38961785fcaa4b49bd6de13371d4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:30:52 -0600 Subject: [PATCH 037/185] Refactor validTokenKeys --- .../filtered_search_manager.js.es6 | 51 +++------------- .../filtered_search_token_keys.js.es6 | 45 ++++++++++++++ .../filtered_search_tokenizer.es6 | 60 +++++++++---------- 3 files changed, 81 insertions(+), 75 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 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 8568bf78416..3899181a352 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,42 +1,5 @@ /* eslint-disable no-param-reassign */ ((global) => { - const validTokenKeys = [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - conditions: [{ - keyword: 'none', - url: 'assignee_id=0', - }], - }, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - conditions: [{ - keyword: 'none', - url: 'milestone_title=No+Milestone', - }, { - keyword: 'upcoming', - url: 'milestone_title=%23upcoming', - }], - }, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - conditions: [{ - keyword: 'none', - url: 'label_name[]=No+Label', - }], - }]; - function clearSearch(e) { e.stopPropagation(); e.preventDefault(); @@ -66,9 +29,9 @@ const key = decodeURIComponent(split[0]); const value = split[1]; - // Check if it matches edge conditions listed in validTokenKeys + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys.get() let conditionIndex = 0; - const validCondition = validTokenKeys + const validCondition = gl.FilteredSearchTokenKeys.get() .filter(v => v.conditions && v.conditions.filter((c, index) => { if (c.url === p) { conditionIndex = index; @@ -82,7 +45,7 @@ // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; - const match = validTokenKeys.filter(t => key === `${t.key}_${t.param}`)[0]; + const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; if (match) { const sanitizedKey = key.slice(0, key.indexOf('_')); @@ -116,7 +79,7 @@ class FilteredSearchManager { constructor() { - this.tokenizer = new gl.FilteredSearchTokenizer(validTokenKeys); + this.tokenizer = gl.FilteredSearchTokenizer; this.bindEvents(); loadSearchParamsFromURL(); } @@ -131,6 +94,7 @@ document.querySelector('.clear-search').addEventListener('click', clearSearch); } + // TODO: This is only used for testing, remove when going to PRO processInput(e) { const input = e.target.value; this.tokenizer.processTokens(input); @@ -155,8 +119,7 @@ const defaultState = 'opened'; let currentState = defaultState; - const tokens = this.tokenizer.getTokens(); - const searchToken = this.tokenizer.getSearchToken(); + const { tokens, searchToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); if (stateIndex !== -1) { const remaining = currentPath.slice(stateIndex + 6); @@ -167,7 +130,7 @@ path += `&state=${currentState}`; tokens.forEach((token) => { - const match = validTokenKeys.filter(t => t.key === token.key)[0]; + const match = gl.FilteredSearchTokenKeys.get().filter(t => t.key === token.key)[0]; let tokenPath = ''; if (token.wildcard && match.conditions) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 new file mode 100644 index 00000000000..8d38a29a354 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -0,0 +1,45 @@ +/* eslint-disable no-param-reassign */ +((global) => { + class FilteredSearchTokenKeys { + static get() { + return [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + conditions: [{ + keyword: 'none', + url: 'assignee_id=0', + }], + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + conditions: [{ + keyword: 'none', + url: 'milestone_title=No+Milestone', + }, { + keyword: 'upcoming', + url: 'milestone_title=%23upcoming', + }], + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + conditions: [{ + keyword: 'none', + url: 'label_name[]=No+Label', + }], + }]; + } + } + + global.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index eab805c4714..b1f37443aa1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -1,33 +1,20 @@ /* eslint-disable no-param-reassign */ ((global) => { class FilteredSearchTokenizer { - constructor(validTokenKeys) { - this.validTokenKeys = validTokenKeys; - this.resetTokens(); - } - - getTokens() { - return this.tokens; - } - - getSearchToken() { - return this.searchToken; - } - - resetTokens() { - this.tokens = []; - this.searchToken = ''; - } - - printTokens() { + // TODO: Remove when going to pro + static printTokens(tokens, searchToken, lastToken) { console.log('tokens:'); - this.tokens.forEach(token => console.log(token)); - console.log(`search: ${this.searchToken}`); + tokens.forEach(token => console.log(token)); + console.log(`search: ${searchToken}`); + console.log('last token:'); + console.log(lastToken); } - processTokens(input) { - // Re-calculate tokens - this.resetTokens(); + static processTokens(input) { + let tokens = []; + let searchToken = ''; + let lastToken = ''; + const validTokenKeys = gl.FilteredSearchTokenKeys.get(); const inputs = input.split(' '); let searchTerms = ''; @@ -36,16 +23,17 @@ inputs.forEach((i) => { if (incompleteToken) { - const prevToken = this.tokens.last(); + const prevToken = tokens.last(); prevToken.value += ` ${i}`; // Remove last quotation const lastQuotationRegex = new RegExp(lastQuotation, 'g'); prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); - this.tokens[this.tokens.length - 1] = prevToken; + tokens[tokens.length - 1] = prevToken; // Check to see if this quotation completes the token value if (i.indexOf(lastQuotation)) { + lastToken = tokens.last(); incompleteToken = !incompleteToken; } @@ -59,8 +47,8 @@ const tokenValue = i.slice(colonIndex + 1); const tokenSymbol = tokenValue[0]; console.log(tokenSymbol) - const keyMatch = this.validTokenKeys.filter(v => v.key === tokenKey)[0]; - const symbolMatch = this.validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; + const keyMatch = validTokenKeys.filter(v => v.key === tokenKey)[0]; + const symbolMatch = validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; const doubleQuoteIndex = tokenValue.indexOf('"'); const singleQuoteIndex = tokenValue.indexOf('\''); @@ -81,11 +69,12 @@ } if (keyMatch && tokenValue.length > 0) { - this.tokens.push({ + tokens.push({ key: keyMatch.key, value: tokenValue, wildcard: symbolMatch ? false : true, }); + lastToken = tokens.last(); return; } @@ -93,10 +82,19 @@ // Add space for next term searchTerms += `${i} `; + lastToken = i; }, this); - this.searchToken = searchTerms.trim(); - this.printTokens(); + searchToken = searchTerms.trim(); + + // TODO: Remove when going to PRO + gl.FilteredSearchTokenizer.printTokens(tokens, searchToken, lastToken); + + return { + tokens, + searchToken, + lastToken, + }; } } From 3492ff6784ffdd72db2863aa982426b29245ed69 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:32:10 -0600 Subject: [PATCH 038/185] Add static methods for dropdowns to interface with --- .../filtered_search_manager.js.es6 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 3899181a352..09a7779635f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -84,6 +84,21 @@ loadSearchParamsFromURL(); } + static fillInWord(word) { + const originalValue = document.querySelector('.filtered-search').value; + document.querySelector('.filtered-search').value = `${originalValue} ${word.trim()}`; + } + + static loadDropdown(dropdownName) { + dropdownName = dropdownName.toLowerCase(); + + const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; + + if (match) { + console.log(`🦄 load ${match.key} dropdown`); + } + } + bindEvents() { const filteredSearchInput = document.querySelector('.filtered-search'); From 44187782bfc7944b535e3feda05557831518806b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 12:48:54 -0600 Subject: [PATCH 039/185] Add type button for accessibility --- app/views/shared/issuable/_search_bar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 5e759301a04..4c27c835bee 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -14,7 +14,7 @@ .filtered-search-input-container %input.form-control.filtered-search{ placeholder: 'Search or filter results...' } = icon('filter') - %button.clear-search.hidden + %button.clear-search.hidden{ type: 'button' } = icon('times') .pull-right - if boards_page From 64d46a3e80001c2dc13f6fd04e2abac40ee9d093 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 15:18:17 -0600 Subject: [PATCH 040/185] Add logic for dynamically selecting which dropdown to load [skip ci] --- .../filtered_search_manager.js.es6 | 55 +++++++++++++++---- .../filtered_search_tokenizer.es6 | 35 +++++++++--- .../shared/issuable/_search_bar.html.haml | 2 +- 3 files changed, 72 insertions(+), 20 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 09a7779635f..8903f382c18 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -77,42 +77,77 @@ } } + let dropdownHint; + class FilteredSearchManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; this.bindEvents(); loadSearchParamsFromURL(); + this.setDropdown(); } - static fillInWord(word) { - const originalValue = document.querySelector('.filtered-search').value; - document.querySelector('.filtered-search').value = `${originalValue} ${word.trim()}`; + static addWordToInput(word, addSpace) { + const hasExistingValue = document.querySelector('.filtered-search').value.length !== 0; + document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; } - static loadDropdown(dropdownName) { + loadDropdown(dropdownName = '') { dropdownName = dropdownName.toLowerCase(); const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; - if (match) { + if (match && this.currentDropdown !== match.key) { console.log(`🦄 load ${match.key} dropdown`); + this.currentDropdown = match.key; + } else if (!match && this.currentDropdown !== 'hint') { + console.log('🦄 load hint dropdown'); + this.currentDropdown = 'hint'; + + if (!dropdownHint) { + dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search')) + } + + dropdownHint.render(); + } + } + + setDropdown() { + const { lastToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); + + if (typeof lastToken === 'string') { + // Token is not fully initialized yet + // because it has no value + // Eg. token = 'label:' + const { tokenKey } = this.tokenizer.parseToken(lastToken); + this.loadDropdown(tokenKey); + } else if (lastToken.hasOwnProperty('key')) { + // Token has been initialized into an object + // because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); } } bindEvents() { const filteredSearchInput = document.querySelector('.filtered-search'); - filteredSearchInput.addEventListener('input', this.processInput.bind(this)); + filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); filteredSearchInput.addEventListener('input', toggleClearSearchButton); filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - document.querySelector('.clear-search').addEventListener('click', clearSearch); } - // TODO: This is only used for testing, remove when going to PRO - processInput(e) { + checkDropdownToken(e) { const input = e.target.value; - this.tokenizer.processTokens(input); + const { lastToken } = this.tokenizer.processTokens(input); + + // Check for dropdown token + if (lastToken[lastToken.length - 1] === ':') { + const token = lastToken.slice(0, -1); + + } } checkForEnter(e) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index b1f37443aa1..b686a43cf32 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -3,11 +3,30 @@ class FilteredSearchTokenizer { // TODO: Remove when going to pro static printTokens(tokens, searchToken, lastToken) { - console.log('tokens:'); - tokens.forEach(token => console.log(token)); - console.log(`search: ${searchToken}`); - console.log('last token:'); - console.log(lastToken); + // console.log('tokens:'); + // tokens.forEach(token => console.log(token)); + // console.log(`search: ${searchToken}`); + // console.log('last token:'); + // console.log(lastToken); + } + + static parseToken(input) { + const colonIndex = input.indexOf(':'); + let tokenKey; + let tokenValue; + let tokenSymbol; + + if (colonIndex !== -1) { + tokenKey = input.slice(0, colonIndex).toLowerCase(); + tokenValue = input.slice(colonIndex + 1); + tokenSymbol = tokenValue[0]; + } + + return { + tokenKey, + tokenValue, + tokenSymbol, + } } static processTokens(input) { @@ -43,10 +62,8 @@ const colonIndex = i.indexOf(':'); if (colonIndex !== -1) { - const tokenKey = i.slice(0, colonIndex).toLowerCase(); - const tokenValue = i.slice(colonIndex + 1); - const tokenSymbol = tokenValue[0]; - console.log(tokenSymbol) + const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i); + const keyMatch = validTokenKeys.filter(v => v.key === tokenKey)[0]; const symbolMatch = validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 4c27c835bee..a45af053f5c 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -12,7 +12,7 @@ class: "check_all_issues left" .issues-other-filters.filtered-search-container .filtered-search-input-container - %input.form-control.filtered-search{ placeholder: 'Search or filter results...' } + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search' } = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') From 3c0755809f82b4eed5913f1994f57ccffffb4686 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 30 Nov 2016 15:25:10 -0600 Subject: [PATCH 041/185] Add dropdown hint --- .../filtered_search/dropdown_hint.js.es6 | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 app/assets/javascripts/filtered_search/dropdown_hint.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 new file mode 100644 index 00000000000..ebbd43ad8e0 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -0,0 +1,106 @@ +/* eslint-disable no-param-reassign */ +((global) => { + const dropdownData = [{ + icon: 'fa-search', + hint: 'Keep typing and press Enter', + tag: '', + },{ + icon: 'fa-pencil', + hint: 'author:', + tag: '<author>' + },{ + icon: 'fa-user', + hint: 'assignee:', + tag: '<assignee>', + },{ + icon: 'fa-clock-o', + hint: 'milestone:', + tag: '<milestone>', + },{ + icon: 'fa-tag', + hint: 'label:', + tag: '<label>', + }]; + + class DropdownHint { + constructor(dropdown, input) { + this.input = input; + this.dropdown = dropdown; + this.bindEvents(); + } + + bindEvents() { + this.dropdown.addEventListener('click.dl', this.itemClicked.bind(this)); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); + } + + // cleanup() { + // this.unbindEvents(); + // droplab.setConfig({'filtered-search': {}}); + // droplab.setData('filtered-search', []); + // this.dropdown.style.display = 'hidden'; + // } + + getSelectedText(selectedToken) { + // TODO: Get last word from FilteredSearchTokenizer + const lastWord = this.input.value.split(' ').last(); + const lastWordIndex = selectedToken.indexOf(lastWord); + + return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); + } + + itemClicked(e) { + const token = e.detail.selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = e.detail.selected.querySelector('.js-filter-tag').innerText.trim(); + + if (tag.length) { + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); + } + + this.input.focus(); + this.dismissDropdown(); + + // Propogate input change to FilteredSearchManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } + + dismissDropdown() { + this.input.removeAttribute('data-dropdown-trigger'); + droplab.setConfig({'filtered-search': {}}); + droplab.setData('filtered-search', []); + this.unbindEvents(); + } + + setAsDropdown() { + this.input.setAttribute('data-dropdown-trigger', '#js-dropdown-hint'); + // const hookId = 'filtered-search'; + // const listId = 'js-dropdown-hint'; + // const hook = droplab.hooks.filter((h) => { + // return h.id === hookId; + // })[0]; + + // if (hook.list.list.id !== listId) { + // droplab.changeHookList(hookId, `#${listId}`); + // } + } + + render() { + console.log('render dropdown hint'); + this.setAsDropdown(); + + droplab.setConfig({ + 'filtered-search': { + text: 'hint' + } + }); + + droplab.setData('filtered-search', dropdownData); + } + } + + global.DropdownHint = DropdownHint; +})(window.gl || (window.gl = {})); From a1ca5c76ab44e306fb4fb4adcfe5ea2214bd5abc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 15:02:54 -0600 Subject: [PATCH 042/185] Add droplab updates --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/droplab/droplab.js | 98 +++++++++++++++---- .../javascripts/droplab/droplab_filter.js | 12 +-- 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e43afbb4cc9..f0615481ed2 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -58,6 +58,7 @@ /*= require_directory ./extensions */ /*= require_directory ./lib/utils */ /*= require_directory ./u2f */ +/*= require_directory ./droplab */ /*= require_directory . */ /*= require fuzzaldrin-plus */ /*= require es6-promise.auto */ diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 18ca8be7203..56582e71b61 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -38,6 +38,7 @@ var DropDown = function(list, trigger) { this.items = []; this.getItems(); this.addEvents(); + this.initialState = list.innerHTML; }; Object.assign(DropDown.prototype, { @@ -50,7 +51,8 @@ Object.assign(DropDown.prototype, { var self = this; // event delegation. this.list.addEventListener('click', function(e) { - if(e.target.tagName === 'A') { + if(e.target.tagName === 'A' || e.target.tagName === 'button') { + e.preventDefault(); self.hide(); var listEvent = new CustomEvent('click.dl', { detail: { @@ -72,6 +74,11 @@ Object.assign(DropDown.prototype, { } }, + setData: function(data) { + this.data = data; + this.render(data); + }, + addData: function(data) { this.data = (this.data || []).concat(data); this.render(data); @@ -155,8 +162,17 @@ require('./window')(function(w){ addData: function () { var args = [].slice.apply(arguments); + this.applyArgs(args, '_addData'); + }, + + setData: function() { + var args = [].slice.apply(arguments); + this.applyArgs(args, '_setData'); + }, + + applyArgs: function(args, methodName) { if(this.ready) { - this._addData.apply(this, args); + this[methodName].apply(this, args); } else { this.queuedData = this.queuedData || []; this.queuedData.push(args); @@ -164,10 +180,18 @@ require('./window')(function(w){ }, _addData: function(trigger, data) { + this._processData(trigger, data, 'addData'); + }, + + _setData: function(trigger, data) { + this._processData(trigger, data, 'setData'); + }, + + _processData: function(trigger, data, methodName) { this.hooks.forEach(function(hook) { if(hook.trigger.dataset.hasOwnProperty('id')) { if(hook.trigger.dataset.id === trigger) { - hook.list.addData(data); + hook.list[methodName](data); } } }); @@ -189,21 +213,48 @@ require('./window')(function(w){ }); }, - addHook: function(hook) { + changeHookList: function(trigger, list) { + trigger = document.querySelector('[data-id="'+trigger+'"]'); + list = document.querySelector(list); + this.hooks.every(function(hook, i) { + if(hook.trigger === trigger) { + // Restore initial State + hook.list.list.innerHTML = hook.list.initialState; + hook.list.hide(); + hook.trigger.removeEventListener('mousedown', hook.events.mousedown); + hook.trigger.removeEventListener('input', hook.events.input); + hook.trigger.removeEventListener('keyup', hook.events.keyup); + hook.trigger.removeEventListener('keydown', hook.events.keydown); + this.hooks.splice(i, 1); + this.addHook(trigger, list); + return false; + } + return true + }.bind(this)); + }, + + addHook: function(hook, list) { if(!(hook instanceof HTMLElement) && typeof hook === 'string'){ hook = document.querySelector(hook); } - var list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); - if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { - this.hooks.push(new HookButton(hook, list)); - } else if(hook.tagName === 'INPUT') { - this.hooks.push(new HookInput(hook, list)); + if(!list){ + list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); + } + + if(hook) { + if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { + this.hooks.push(new HookButton(hook, list)); + } else if(hook.tagName === 'INPUT') { + this.hooks.push(new HookInput(hook, list)); + } } return this; }, addHooks: function(hooks) { - hooks.forEach(this.addHook.bind(this)); + hooks.forEach(function(hook) { + this.addHook(hook, null); + }.bind(this)); return this; }, @@ -302,7 +353,8 @@ var HookInput = function(trigger, list) { Object.assign(HookInput.prototype, { addEvents: function(){ var self = this; - this.trigger.addEventListener('mousedown', function(e){ + + function mousedown(e) { var mouseEvent = new CustomEvent('mousedown.dl', { detail: { hook: self, @@ -312,9 +364,9 @@ Object.assign(HookInput.prototype, { cancelable: true }); e.target.dispatchEvent(mouseEvent); - }); + } - this.trigger.addEventListener('input', function(e){ + function input(e) { var inputEvent = new CustomEvent('input.dl', { detail: { hook: self, @@ -325,15 +377,15 @@ Object.assign(HookInput.prototype, { }); e.target.dispatchEvent(inputEvent); self.list.show(); - }); + } - this.trigger.addEventListener('keyup', function(e){ + function keyup(e) { keyEvent(e, 'keyup.dl'); - }); + } - this.trigger.addEventListener('keydown', function(e){ + function keydown(e) { keyEvent(e, 'keydown.dl'); - }); + } function keyEvent(e, keyEventName){ var keyEvent = new CustomEvent(keyEventName, { @@ -349,6 +401,16 @@ Object.assign(HookInput.prototype, { e.target.dispatchEvent(keyEvent); self.list.show(); } + + this.events = this.events || {}; + this.events.mousedown = mousedown; + this.events.input = input; + this.events.keyup = keyup; + this.events.keydown = keydown; + this.trigger.addEventListener('mousedown', mousedown); + this.trigger.addEventListener('input', input); + this.trigger.addEventListener('keyup', keyup); + this.trigger.addEventListener('keydown', keydown); }, }); diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js index 4a7ae0cbdc1..88e69c02422 100644 --- a/app/assets/javascripts/droplab/droplab_filter.js +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -2,18 +2,17 @@ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Fri, 2 Dec 2016 15:04:10 -0600 Subject: [PATCH 043/185] Add dropdowns for assignee --- .../filtered_search/dropdown_assignee.js.es6 | 21 +++++ .../filtered_search/dropdown_hint.js.es6 | 69 +++------------- .../filtered_search_dropdown.js.es6 | 78 +++++++++++++++++++ .../filtered_search_manager.js.es6 | 22 +++++- .../shared/issuable/_search_bar.html.haml | 17 ++++ 5 files changed, 147 insertions(+), 60 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 create mode 100644 app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 new file mode 100644 index 00000000000..9e4d1018ac3 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -0,0 +1,21 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownAssignee extends gl.FilteredSearchDropdown { + constructor(dropdown, input) { + super(dropdown, input); + this.listId = 'js-dropdown-assignee'; + } + + itemClicked(e) { + console.log('assignee clicked'); + } + + renderContent() { + droplab.addData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + } + } + + global.DropdownAssignee = DropdownAssignee; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index ebbd43ad8e0..0593561c8a1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -1,4 +1,6 @@ /* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + ((global) => { const dropdownData = [{ icon: 'fa-search', @@ -22,34 +24,11 @@ tag: '<label>', }]; - class DropdownHint { - constructor(dropdown, input) { - this.input = input; - this.dropdown = dropdown; - this.bindEvents(); - } - - bindEvents() { - this.dropdown.addEventListener('click.dl', this.itemClicked.bind(this)); - } - - unbindEvents() { - this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); - } - - // cleanup() { - // this.unbindEvents(); - // droplab.setConfig({'filtered-search': {}}); - // droplab.setData('filtered-search', []); - // this.dropdown.style.display = 'hidden'; - // } - - getSelectedText(selectedToken) { - // TODO: Get last word from FilteredSearchTokenizer - const lastWord = this.input.value.split(' ').last(); - const lastWordIndex = selectedToken.indexOf(lastWord); - - return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); + class DropdownHint extends gl.FilteredSearchDropdown { + constructor(dropdown, input, filterKeyword) { + super(dropdown, input); + this.listId = 'js-dropdown-hint'; + this.filterKeyword = filterKeyword; } itemClicked(e) { @@ -68,37 +47,9 @@ this.input.dispatchEvent(new Event('input')); } - dismissDropdown() { - this.input.removeAttribute('data-dropdown-trigger'); - droplab.setConfig({'filtered-search': {}}); - droplab.setData('filtered-search', []); - this.unbindEvents(); - } - - setAsDropdown() { - this.input.setAttribute('data-dropdown-trigger', '#js-dropdown-hint'); - // const hookId = 'filtered-search'; - // const listId = 'js-dropdown-hint'; - // const hook = droplab.hooks.filter((h) => { - // return h.id === hookId; - // })[0]; - - // if (hook.list.list.id !== listId) { - // droplab.changeHookList(hookId, `#${listId}`); - // } - } - - render() { - console.log('render dropdown hint'); - this.setAsDropdown(); - - droplab.setConfig({ - 'filtered-search': { - text: 'hint' - } - }); - - droplab.setData('filtered-search', dropdownData); + renderContent() { + super.renderContent(); + droplab.setData(this.hookId, dropdownData); } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 new file mode 100644 index 00000000000..250d8236ea9 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -0,0 +1,78 @@ +/* eslint-disable no-param-reassign */ +((global) => { + const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; + + class FilteredSearchDropdown { + constructor(dropdown, input) { + this.hookId = 'filtered-search'; + this.input = input; + this.dropdown = dropdown; + this.bindEvents(); + } + + bindEvents() { + this.dropdown.addEventListener('click.dl', this.itemClicked.bind(this)); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); + } + + getSelectedText(selectedToken) { + // TODO: Get last word from FilteredSearchTokenizer + const lastWord = this.input.value.split(' ').last(); + const lastWordIndex = selectedToken.indexOf(lastWord); + + return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); + } + + itemClicked(e) { + // Overridden by dropdown sub class + } + + getFilterConfig(filterKeyword) { + const config = {}; + const filterConfig = { + text: filterKeyword, + }; + + config[this.hookId] = filterKeyword ? filterConfig : {}; + + return config; + } + + dismissDropdown() { + this.input.removeAttribute(DATA_DROPDOWN_TRIGGER); + droplab.setConfig(this.getFilterConfig()); + droplab.setData(this.hookId, []); + this.unbindEvents(); + } + + setAsDropdown() { + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.listId}`); + } + + getCurrentHook() { + return droplab.hooks.filter(h => h.id === this.hookId)[0]; + } + + renderContent() { + droplab.setConfig(this.getFilterConfig(this.filterKeyword)); + } + + render() { + this.setAsDropdown(); + + const firstTimeInitialized = this.getCurrentHook() === undefined; + + if (firstTimeInitialized) { + this.renderContent(); + } else if(this.getCurrentHook().list.list.id !== this.listId) { + droplab.changeHookList(this.hookId, `#${this.listId}`); + this.renderContent(); + } + } + } + + global.FilteredSearchDropdown = FilteredSearchDropdown; +})(window.gl || (window.gl = {})); 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 8903f382c18..92f07024354 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -78,6 +78,7 @@ } let dropdownHint; + let dropdownAssignee; class FilteredSearchManager { constructor() { @@ -99,19 +100,38 @@ if (match && this.currentDropdown !== match.key) { console.log(`🦄 load ${match.key} dropdown`); + this.dismissCurrentDropdown(); this.currentDropdown = match.key; + + if (match.key === 'assignee') { + if (!dropdownAssignee) { + + // document.querySelector('.filtered-search').setAttribute('data-dropdown-trigger', '#js-dropdown-assignee'); + dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), document.querySelector('.filtered-search')); + } + + dropdownAssignee.render(); + } + } else if (!match && this.currentDropdown !== 'hint') { console.log('🦄 load hint dropdown'); + this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search')) + dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), 'hint'); } dropdownHint.render(); } } + dismissCurrentDropdown() { + if (this.currentDropdown === 'hint') { + dropdownHint.dismissDropdown(); + } + } + setDropdown() { const { lastToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index a45af053f5c..04000a18dce 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -16,6 +16,23 @@ = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') + %ul#js-dropdown-hint.dropdown-menu{ 'data-dynamic' => true } + %li + %button.btn.btn-link + %i.fa{ 'class': '{{icon}}'} + %span.js-filter-hint + {{hint}} + %span.js-filter-tag + {{tag}} + #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dynamic' => true } + %li + %button.btn.btn-link + %img.avatar.avatar-inline{ 'src': '{{avatar_url}}', width: '30' } + %strong + {{name}} + %span + {{username}} .pull-right - if boards_page #js-boards-seach.issue-boards-search From 9081d3efeec5b22fd92c76172ae92dad3cc94c58 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:20:01 -0600 Subject: [PATCH 044/185] Update droplab --- app/assets/javascripts/droplab/droplab.js | 19 +++++++++++++++++-- .../javascripts/droplab/droplab_ajax.js | 11 +++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 56582e71b61..aff47aa23cf 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -51,13 +51,15 @@ Object.assign(DropDown.prototype, { var self = this; // event delegation. this.list.addEventListener('click', function(e) { - if(e.target.tagName === 'A' || e.target.tagName === 'button') { + // climb up the tree to find the LI + var selected = utils.closest(e.target, 'LI'); + if(selected) { e.preventDefault(); self.hide(); var listEvent = new CustomEvent('click.dl', { detail: { list: self, - selected: e.target, + selected: selected, data: e.target.dataset, }, }); @@ -102,6 +104,15 @@ Object.assign(DropDown.prototype, { var html = utils.t(sampleItem.outerHTML, dat); var template = document.createElement('template'); template.innerHTML = html; + + // Help set the image src template + var imageTags = template.content.querySelectorAll('img[data-src]'); + for(var i = 0; i < imageTags.length; i++) { + var imageTag = imagetags[i]; + imageTag.src = imageTag.getAttribute('data-src'); + imageTag.removeAttribute('data-src'); + } + if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){ template.content.firstChild.style.display = 'none' }else{ @@ -115,6 +126,9 @@ Object.assign(DropDown.prototype, { } else { this.list.innerHTML = newChildren.join(''); } + + // Show dropdown if there is data + data !== [] ? this.show() : this.hide(); }, show: function() { @@ -221,6 +235,7 @@ require('./window')(function(w){ // Restore initial State hook.list.list.innerHTML = hook.list.initialState; hook.list.hide(); + hook.trigger.removeEventListener('mousedown', hook.events.mousedown); hook.trigger.removeEventListener('input', hook.events.input); hook.trigger.removeEventListener('keyup', hook.events.keyup); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 23e43b352d6..2dff5b83fae 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -3,6 +3,7 @@ /* global droplab */ droplab.plugin(function init(DropLab) { var _addData = DropLab.prototype.addData; + var _setData = DropLab.prototype.setData; var _loadUrlData = function(url) { return new Promise(function(resolve, reject) { @@ -24,10 +25,16 @@ droplab.plugin(function init(DropLab) { Object.assign(DropLab.prototype, { addData: function(trigger, data) { + this.processData(trigger, data, _addData); + }, + setData: function(trigger, data) { + this.processData(trigger, data, _setData); + }, + processData: function(trigger, data, methodName) { var _this = this; if('string' === typeof data) { _loadUrlData(data).then(function(d) { - _addData.call(_this, trigger, d); + methodName.call(_this, trigger, d); }).catch(function(e) { if(e.message) console.error(e.message, e.stack); // eslint-disable-line no-console @@ -35,7 +42,7 @@ droplab.plugin(function init(DropLab) { console.error(e); // eslint-disable-line no-console }) } else { - _addData.apply(this, arguments); + methodName.apply(this, arguments); } }, }); From ce1247727d2a9f24994f602debe95fd2ebff90db Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:20:21 -0600 Subject: [PATCH 045/185] Fix rendering of assignee dropdown after clicking hint dropdown --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 3 ++- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index 9e4d1018ac3..e3cbb4cb3a0 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -13,7 +13,8 @@ } renderContent() { - droplab.addData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + super.renderContent(); + droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); } } 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 92f07024354..fc7bfe121fb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -121,7 +121,6 @@ if (!dropdownHint) { dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), 'hint'); } - dropdownHint.render(); } } From 78b9e7c6b500b87dd2ebac45331a012eb0cf6ca4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:30:19 -0600 Subject: [PATCH 046/185] Add author dropdown --- .../filtered_search/dropdown_author.js.es6 | 22 +++++++++++++++++++ .../filtered_search_manager.js.es6 | 13 +++++++---- .../shared/issuable/_search_bar.html.haml | 9 ++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/dropdown_author.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 new file mode 100644 index 00000000000..e16b313b743 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -0,0 +1,22 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownAuthor extends gl.FilteredSearchDropdown { + constructor(dropdown, input) { + super(dropdown, input); + this.listId = 'js-dropdown-author'; + } + + itemClicked(e) { + console.log('author clicked'); + } + + renderContent() { + super.renderContent(); + droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + } + } + + global.DropdownAuthor = DropdownAuthor; +})(window.gl || (window.gl = {})); 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 fc7bfe121fb..237f4fc3fff 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -78,6 +78,7 @@ } let dropdownHint; + let dropdownAuthor; let dropdownAssignee; class FilteredSearchManager { @@ -103,10 +104,14 @@ this.dismissCurrentDropdown(); this.currentDropdown = match.key; - if (match.key === 'assignee') { - if (!dropdownAssignee) { + if (match.key === 'author') { + if (!dropdownAuthor) { + dropdownAuthor = new gl.DropdownAuthor(document.querySelector('#js-dropdown-author'), document.querySelector('.filtered-search')); + } - // document.querySelector('.filtered-search').setAttribute('data-dropdown-trigger', '#js-dropdown-assignee'); + dropdownAuthor.render(); + } else if (match.key === 'assignee') { + if (!dropdownAssignee) { dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), document.querySelector('.filtered-search')); } @@ -119,7 +124,7 @@ this.currentDropdown = 'hint'; if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), 'hint'); + dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), this.currentDropdown); } dropdownHint.render(); } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 04000a18dce..3801b46a332 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -24,6 +24,15 @@ {{hint}} %span.js-filter-tag {{tag}} + #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dynamic' => true } + %li + %button.btn.btn-link + %img.avatar.avatar-inline{ 'src': '{{avatar_url}}', width: '30' } + %strong + {{name}} + %span + {{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } %li From a510791bb2fb537fd2fbe4a9f6b94e38fe5a6094 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 2 Dec 2016 16:43:15 -0600 Subject: [PATCH 047/185] Add label and milestone dropdowns --- .../filtered_search/dropdown_label.js.es6 | 22 +++++++++++++++++++ .../filtered_search/dropdown_milestone.js.es6 | 22 +++++++++++++++++++ .../filtered_search_manager.js.es6 | 14 ++++++++++++ .../shared/issuable/_search_bar.html.haml | 11 ++++++++++ 4 files changed, 69 insertions(+) create mode 100644 app/assets/javascripts/filtered_search/dropdown_label.js.es6 create mode 100644 app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 new file mode 100644 index 00000000000..9225dca13b0 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -0,0 +1,22 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownLabel extends gl.FilteredSearchDropdown { + constructor(dropdown, input) { + super(dropdown, input); + this.listId = 'js-dropdown-label'; + } + + itemClicked(e) { + console.log('label clicked'); + } + + renderContent() { + super.renderContent(); + droplab.setData(this.hookId, 'labels.json'); + } + } + + global.DropdownLabel = DropdownLabel; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 new file mode 100644 index 00000000000..ab97d709886 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -0,0 +1,22 @@ +/* eslint-disable no-param-reassign */ +/*= require filtered_search/filtered_search_dropdown */ + +((global) => { + class DropdownMilestone extends gl.FilteredSearchDropdown { + constructor(dropdown, input) { + super(dropdown, input); + this.listId = 'js-dropdown-milestone'; + } + + itemClicked(e) { + console.log('milestone clicked'); + } + + renderContent() { + super.renderContent(); + droplab.setData(this.hookId, 'milestones.json'); + } + } + + global.DropdownMilestone = DropdownMilestone; +})(window.gl || (window.gl = {})); 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 237f4fc3fff..f06d5a646cf 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -80,6 +80,8 @@ let dropdownHint; let dropdownAuthor; let dropdownAssignee; + let dropdownMilestone; + let dropdownLabel; class FilteredSearchManager { constructor() { @@ -116,6 +118,18 @@ } dropdownAssignee.render(); + } else if (match.key === 'milestone') { + if (!dropdownMilestone) { + dropdownMilestone = new gl.DropdownMilestone(document.querySelector('#js-dropdown-milestone'), document.querySelector('.filtered-search')); + } + + dropdownMilestone.render(); + } else if (match.key === 'label') { + if (!dropdownLabel) { + dropdownLabel = new gl.DropdownLabel(document.querySelector('#js-dropdown-label'), document.querySelector('.filtered-search')); + } + + dropdownLabel.render(); } } else if (!match && this.currentDropdown !== 'hint') { diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 3801b46a332..cf5b1a52332 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -42,6 +42,17 @@ {{name}} %span {{username}} + #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dynamic' => true } + %li + %button.btn.btn-link + {{title}} + #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dynamic' => true } + %li + %button.btn.btn-link + %span.dropdown-label-box{ 'style': 'background: {{color}}'} + {{title}} .pull-right - if boards_page #js-boards-seach.issue-boards-search From 2edaabfe669f7e865a56eab321db41fe9cdcad89 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:18:02 -0600 Subject: [PATCH 048/185] Fix image data-src --- app/assets/javascripts/droplab/droplab.js | 2 +- app/views/shared/issuable/_search_bar.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index aff47aa23cf..0152eef793f 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -108,7 +108,7 @@ Object.assign(DropDown.prototype, { // Help set the image src template var imageTags = template.content.querySelectorAll('img[data-src]'); for(var i = 0; i < imageTags.length; i++) { - var imageTag = imagetags[i]; + var imageTag = imageTags[i]; imageTag.src = imageTag.getAttribute('data-src'); imageTag.removeAttribute('data-src'); } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index cf5b1a52332..c7841486ad1 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -28,7 +28,7 @@ %ul{ 'data-dynamic' => true } %li %button.btn.btn-link - %img.avatar.avatar-inline{ 'src': '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } %strong {{name}} %span @@ -37,7 +37,7 @@ %ul{ 'data-dynamic' => true } %li %button.btn.btn-link - %img.avatar.avatar-inline{ 'src': '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } %strong {{name}} %span From f19503b008e962a6eaf16ce4fa18bdb89ceb7442 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:26:36 -0600 Subject: [PATCH 049/185] Style hint dropdown --- .../filtered_search/dropdown_hint.js.es6 | 4 ++-- app/assets/stylesheets/framework/filters.scss | 22 +++++++++++++++++++ .../shared/issuable/_search_bar.html.haml | 6 ++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 0593561c8a1..dc28b97fea9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -32,8 +32,8 @@ } itemClicked(e) { - const token = e.detail.selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = e.detail.selected.querySelector('.js-filter-tag').innerText.trim(); + const token = e.detail.selected.querySelector('.dropdown-filter-hint').innerText.trim(); + const tag = e.detail.selected.querySelector('.dropdown-filter-tag').innerText.trim(); if (tag.length) { gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index c679a3833e9..71b33646185 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -68,3 +68,25 @@ } } } + +.dropdown-menu .filter-dropdown { + padding: 0; +} + +.filter-dropdown { + .btn { + border: none; + width: 100%; + text-align: left; + padding: 8px 16px; + + &:hover { + text-decoration: none; + } + } + + .dropdown-filter-tag { + font-size: 14px; + font-weight: 400; + } +} diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index c7841486ad1..6df35b78194 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -17,12 +17,12 @@ %button.clear-search.hidden{ type: 'button' } = icon('times') %ul#js-dropdown-hint.dropdown-menu{ 'data-dynamic' => true } - %li + %li.filter-dropdown %button.btn.btn-link %i.fa{ 'class': '{{icon}}'} - %span.js-filter-hint + %span.dropdown-filter-hint {{hint}} - %span.js-filter-tag + %span.dropdown-filter-tag {{tag}} #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } From fdd1bac91a0554df9a3a25b500877d376fd6b2a0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 12:35:28 -0600 Subject: [PATCH 050/185] Style author dropdown --- .../filtered_search/dropdown_hint.js.es6 | 4 ++-- app/assets/stylesheets/framework/filters.scss | 11 ++++++++- .../shared/issuable/_search_bar.html.haml | 23 ++++++++++--------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index dc28b97fea9..0593561c8a1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -32,8 +32,8 @@ } itemClicked(e) { - const token = e.detail.selected.querySelector('.dropdown-filter-hint').innerText.trim(); - const tag = e.detail.selected.querySelector('.dropdown-filter-tag').innerText.trim(); + const token = e.detail.selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = e.detail.selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 71b33646185..767803ac1d0 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -85,8 +85,17 @@ } } - .dropdown-filter-tag { + .dropdown-light-content { font-size: 14px; font-weight: 400; } + + .dropdown-user { + display: flex; + } + + .dropdown-user-details { + display: flex; + flex-direction: column; + } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 6df35b78194..b83ea6c60d9 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -20,22 +20,23 @@ %li.filter-dropdown %button.btn.btn-link %i.fa{ 'class': '{{icon}}'} - %span.dropdown-filter-hint + %span.js-filter-hint {{hint}} - %span.dropdown-filter-tag + %span.js-filter-tag.dropdown-light-content {{tag}} #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } - %li - %button.btn.btn-link + %li.filter-dropdown + %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } - %strong - {{name}} - %span - {{username}} + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } - %li + %li.filter-dropdown %button.btn.btn-link %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } %strong @@ -44,12 +45,12 @@ {{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } - %li + %li.filter-dropdown %button.btn.btn-link {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dynamic' => true } - %li + %li.filter-dropdown %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} {{title}} From 7cf322f1d667b6e0d1c74e58a89aa3dbd437a850 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:00:18 -0600 Subject: [PATCH 051/185] Add blue hover for dropdowns --- app/assets/stylesheets/framework/filters.scss | 2 ++ app/assets/stylesheets/framework/variables.scss | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 767803ac1d0..4fa826c1b76 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -81,6 +81,8 @@ padding: 8px 16px; &:hover { + background-color: $dropdown-hover-color; + color: white; text-decoration: none; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 3e52c482ece..f3cb3d33d99 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -263,6 +263,11 @@ $dropdown-chevron-size: 10px; $dropdown-toggle-active-border-color: darken($border-color, 14%); +/* +* Filtered Search +*/ +$dropdown-hover-color: #3B86FF; + /* * Buttons */ From eb55cf5007f478a1fdffe029110c27c869f74356 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:00:54 -0600 Subject: [PATCH 052/185] Add static dropdown list items --- .../shared/issuable/_search_bar.html.haml | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index b83ea6c60d9..a47140ed0aa 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -35,20 +35,36 @@ %span.dropdown-light-content @{{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } - %ul{ 'data-dynamic' => true } + %ul %li.filter-dropdown %button.btn.btn-link + No assignee + %li.divider + %ul{ 'data-dynamic' => true } + %li.filter-dropdown + %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } - %strong - {{name}} - %span - {{username}} + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } + %ul + %li.filter-dropdown + %button.btn.btn-link + No milestone + %li.divider %ul{ 'data-dynamic' => true } %li.filter-dropdown %button.btn.btn-link {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } + %ul + %li.filter-dropdown + %button.btn.btn-link + No label + %li.divider %ul{ 'data-dynamic' => true } %li.filter-dropdown %button.btn.btn-link From 9f1c19ce84046de2e85eff1aef7f693a2925bf11 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:09:29 -0600 Subject: [PATCH 053/185] Style label dropdowns --- app/assets/stylesheets/framework/filters.scss | 2 ++ app/views/shared/issuable/_search_bar.html.haml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 4fa826c1b76..c4b4a56a8b5 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -79,6 +79,8 @@ width: 100%; text-align: left; padding: 8px 16px; + text-overflow: ellipsis; + overflow-y: hidden; &:hover { background-color: $dropdown-hover-color; diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index a47140ed0aa..f076c9c1a75 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -69,7 +69,7 @@ %li.filter-dropdown %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} - {{title}} + {{title}} .pull-right - if boards_page #js-boards-seach.issue-boards-search From 52fd1b08caad773b332216c117bdecc25cbd6256 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:17:43 -0600 Subject: [PATCH 054/185] Add vertical scrolling for dropdowns --- app/assets/stylesheets/framework/filters.scss | 9 +++++++ .../shared/issuable/_search_bar.html.haml | 26 +++++++++---------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index c4b4a56a8b5..2efdb537cb3 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -74,6 +74,11 @@ } .filter-dropdown { + max-height: 215px; + overflow-x: scroll; +} + +.filter-dropdown-item { .btn { border: none; width: 100%; @@ -103,3 +108,7 @@ flex-direction: column; } } + +.hint-dropdown { + width: 250px; +} diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index f076c9c1a75..8dda6e99d2d 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -16,8 +16,8 @@ = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') - %ul#js-dropdown-hint.dropdown-menu{ 'data-dynamic' => true } - %li.filter-dropdown + %ul#js-dropdown-hint.dropdown-menu.hint-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link %i.fa{ 'class': '{{icon}}'} %span.js-filter-hint @@ -25,8 +25,8 @@ %span.js-filter-tag.dropdown-light-content {{tag}} #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } - %ul{ 'data-dynamic' => true } - %li.filter-dropdown + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } .dropdown-user-details @@ -36,12 +36,12 @@ @{{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown + %li.filter-dropdown-item %button.btn.btn-link No assignee %li.divider - %ul{ 'data-dynamic' => true } - %li.filter-dropdown + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } .dropdown-user-details @@ -51,22 +51,22 @@ @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown + %li.filter-dropdown-item %button.btn.btn-link No milestone %li.divider - %ul{ 'data-dynamic' => true } - %li.filter-dropdown + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown + %li.filter-dropdown-item %button.btn.btn-link No label %li.divider - %ul{ 'data-dynamic' => true } - %li.filter-dropdown + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} {{title}} From c0ec94f5f0e0ba24655ab06f60a8da3ccd2930f8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:26:24 -0600 Subject: [PATCH 055/185] Fix css dropdown width --- app/assets/stylesheets/framework/filters.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 2efdb537cb3..1f980c3d618 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -69,7 +69,7 @@ } } -.dropdown-menu .filter-dropdown { +.dropdown-menu .filter-dropdown-item { padding: 0; } From 24042d882b544e8909170bdb49d080c5c9e153fd Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:32:44 -0600 Subject: [PATCH 056/185] Add white background for dropdown label box color --- app/assets/stylesheets/framework/filters.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 1f980c3d618..bcbf0e868e2 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -91,6 +91,12 @@ background-color: $dropdown-hover-color; color: white; text-decoration: none; + + .dropdown-label-box { + border-color: white; + border-style: solid; + border-width: 2px; + } } } From 0e7b8413329270379b6675d87fb5cc28a5b24a58 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 13:34:20 -0600 Subject: [PATCH 057/185] Add focus state style the same as hover state --- app/assets/stylesheets/framework/filters.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index bcbf0e868e2..130d06b601c 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -87,7 +87,8 @@ text-overflow: ellipsis; overflow-y: hidden; - &:hover { + &:hover, + &:focus { background-color: $dropdown-hover-color; color: white; text-decoration: none; From e36d32327185e9e8824b04f0cebcbfbffa42d7e8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 15:12:34 -0600 Subject: [PATCH 058/185] Add dropdown offset to match input cursor --- .../filtered_search_dropdown.js.es6 | 4 ++++ .../filtered_search_manager.js.es6 | 24 +++++++++++++++---- .../javascripts/lib/utils/text_utility.js | 16 +++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) 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 250d8236ea9..251162f3fb1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -52,6 +52,10 @@ this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.listId}`); } + setOffset(offset = 0) { + this.dropdown.style.left = `${offset}px`; + } + getCurrentHook() { return droplab.hooks.filter(h => h.id === this.hookId)[0]; } 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 f06d5a646cf..c80c60d6d6e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -99,47 +99,61 @@ loadDropdown(dropdownName = '') { dropdownName = dropdownName.toLowerCase(); + const filterIconPadding = 27; const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; + const filteredSearch = document.querySelector('.filtered-search'); if (match && this.currentDropdown !== match.key) { console.log(`🦄 load ${match.key} dropdown`); + + const dynamicDropdownPadding = 12; + const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding + dynamicDropdownPadding; + this.dismissCurrentDropdown(); this.currentDropdown = match.key; if (match.key === 'author') { if (!dropdownAuthor) { - dropdownAuthor = new gl.DropdownAuthor(document.querySelector('#js-dropdown-author'), document.querySelector('.filtered-search')); + dropdownAuthor = new gl.DropdownAuthor(document.querySelector('#js-dropdown-author'), filteredSearch); } + dropdownAuthor.setOffset(dropdownOffset); dropdownAuthor.render(); } else if (match.key === 'assignee') { if (!dropdownAssignee) { - dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), document.querySelector('.filtered-search')); + dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), filteredSearch); } + dropdownAssignee.setOffset(dropdownOffset); dropdownAssignee.render(); } else if (match.key === 'milestone') { if (!dropdownMilestone) { - dropdownMilestone = new gl.DropdownMilestone(document.querySelector('#js-dropdown-milestone'), document.querySelector('.filtered-search')); + dropdownMilestone = new gl.DropdownMilestone(document.querySelector('#js-dropdown-milestone'), filteredSearch); } + dropdownMilestone.setOffset(dropdownOffset); dropdownMilestone.render(); } else if (match.key === 'label') { if (!dropdownLabel) { - dropdownLabel = new gl.DropdownLabel(document.querySelector('#js-dropdown-label'), document.querySelector('.filtered-search')); + dropdownLabel = new gl.DropdownLabel(document.querySelector('#js-dropdown-label'), filteredSearch); } + dropdownLabel.setOffset(dropdownOffset); dropdownLabel.render(); } } else if (!match && this.currentDropdown !== 'hint') { console.log('🦄 load hint dropdown'); + + const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding; + this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), document.querySelector('.filtered-search'), this.currentDropdown); + dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), filteredSearch, this.currentDropdown); } + dropdownHint.setOffset(dropdownOffset); dropdownHint.render(); } } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 5066e3282d7..e47eccc3a33 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -17,6 +17,22 @@ gl.text.replaceRange = function(s, start, end, substitute) { return s.substring(0, start) + substitute + s.substring(end); }; + gl.text.getTextWidth = function(text, font) { + /** + * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. + * + * @param {String} text The text to be rendered. + * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). + * + * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 + */ + // re-use canvas object for better performance + var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement("canvas")); + var context = canvas.getContext("2d"); + context.font = font; + var metrics = context.measureText(text); + return metrics.width; + }; gl.text.selectedText = function(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); }; From dd90dd0e28839a0e83a2145aa13c6f517efbee89 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 15:40:55 -0600 Subject: [PATCH 059/185] Set data_dropdown_trigger to empty instead of removing --- .../javascripts/filtered_search/filtered_search_dropdown.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 251162f3fb1..0a406bef985 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -42,7 +42,7 @@ } dismissDropdown() { - this.input.removeAttribute(DATA_DROPDOWN_TRIGGER); + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, ''); droplab.setConfig(this.getFilterConfig()); droplab.setData(this.hookId, []); this.unbindEvents(); From d2ecba6edfea8ff1836943b6c683fdb36e4c92a3 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:04:06 -0600 Subject: [PATCH 060/185] Remove bad droplab code --- app/assets/javascripts/droplab/droplab.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 0152eef793f..6befa0976d4 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -126,9 +126,6 @@ Object.assign(DropDown.prototype, { } else { this.list.innerHTML = newChildren.join(''); } - - // Show dropdown if there is data - data !== [] ? this.show() : this.hide(); }, show: function() { From f5719c2c48dd989ed5431a78e95e560f3c1d9335 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:05:30 -0600 Subject: [PATCH 061/185] Add ability to click on none as an option --- .../filtered_search/dropdown_assignee.js.es6 | 8 +++++++- .../filtered_search/dropdown_hint.js.es6 | 5 ----- .../filtered_search/dropdown_label.js.es6 | 9 ++++++++- .../filtered_search/dropdown_milestone.js.es6 | 8 +++++++- .../filtered_search_dropdown.js.es6 | 19 ++++++++++++++++++- .../filtered_search_manager.js.es6 | 2 +- .../shared/issuable/_search_bar.html.haml | 9 +++++---- 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index e3cbb4cb3a0..fcaacac1b50 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -9,7 +9,13 @@ } itemClicked(e) { - console.log('assignee clicked'); + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + console.log('set value'); + } + + this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 0593561c8a1..b7161d00eb9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -39,12 +39,7 @@ gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); } - this.input.focus(); this.dismissDropdown(); - - // Propogate input change to FilteredSearchManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new Event('input')); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index 9225dca13b0..ef92ecd3bd1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -9,7 +9,14 @@ } itemClicked(e) { - console.log('label clicked'); + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + const labelName = `~${e.detail.selected.querySelector('.label-title').innerText.trim()}`; + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(labelName)); + } + + this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index ab97d709886..00df89ff063 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -9,7 +9,13 @@ } itemClicked(e) { - console.log('milestone clicked'); + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + console.log('set value'); + } + + this.dismissDropdown(); } renderContent() { 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 0a406bef985..a345b368238 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -41,13 +41,20 @@ return config; } - dismissDropdown() { + destroy() { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, ''); droplab.setConfig(this.getFilterConfig()); droplab.setData(this.hookId, []); this.unbindEvents(); } + dismissDropdown() { + this.input.focus(); + // Propogate input change to FilteredSearchManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } + setAsDropdown() { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.listId}`); } @@ -56,6 +63,16 @@ this.dropdown.style.left = `${offset}px`; } + setDataValueIfSelected(selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchManager.addWordToInput(dataValue); + } + + return dataValue !== null; + } + getCurrentHook() { return droplab.hooks.filter(h => h.id === this.hookId)[0]; } 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 c80c60d6d6e..53ab2135a09 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -160,7 +160,7 @@ dismissCurrentDropdown() { if (this.currentDropdown === 'hint') { - dropdownHint.dismissDropdown(); + dropdownHint.destroy(); } } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 8dda6e99d2d..39af0c2c288 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -36,7 +36,7 @@ @{{username}} #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown-item + %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No assignee %li.divider @@ -51,7 +51,7 @@ @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown-item + %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No milestone %li.divider @@ -61,7 +61,7 @@ {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul - %li.filter-dropdown-item + %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No label %li.divider @@ -69,7 +69,8 @@ %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} - {{title}} + %span.label-title + {{title}} .pull-right - if boards_page #js-boards-seach.issue-boards-search From 0c1c26c0bcc5c62f5959d7fc1399d44e1a6617cd Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:29:55 -0600 Subject: [PATCH 062/185] Replace typed token with selected dropdown token --- .../filtered_search/filtered_search_manager.js.es6 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 53ab2135a09..7e6144b571d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -92,7 +92,14 @@ } static addWordToInput(word, addSpace) { - const hasExistingValue = document.querySelector('.filtered-search').value.length !== 0; + const filteredSearchValue = document.querySelector('.filtered-search').value; + const hasExistingValue = filteredSearchValue.length !== 0; + + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); + if (lastToken.hasOwnProperty('key')) { + document.querySelector('.filtered-search').value = filteredSearchValue.slice(0, -1 * (lastToken.value.length)); + } + document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; } From 71da8ffaef9220c10d2b95ca0ae06bc08fefa594 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 5 Dec 2016 16:36:05 -0600 Subject: [PATCH 063/185] Populate selected item in filtered search input --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 3 ++- .../javascripts/filtered_search/dropdown_author.js.es6 | 5 ++++- .../javascripts/filtered_search/dropdown_milestone.js.es6 | 3 ++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index fcaacac1b50..e791de5ad41 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -12,7 +12,8 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - console.log('set value'); + const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); } this.dismissDropdown(); diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index e16b313b743..75eb1c06fbd 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -9,7 +9,10 @@ } itemClicked(e) { - console.log('author clicked'); + const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); + + this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 00df89ff063..8c75bd30e97 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -12,7 +12,8 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - console.log('set value'); + const milestoneName = `%${e.detail.selected.querySelector('.btn-link').innerText.trim()}`; + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(milestoneName)); } this.dismissDropdown(); From bcae21b1badaa3a7aedfc44f67908909b34afb7d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 11:34:00 -0600 Subject: [PATCH 064/185] Remove border radius of list item buttons --- app/assets/stylesheets/framework/filters.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 130d06b601c..0882af57482 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -86,6 +86,7 @@ padding: 8px 16px; text-overflow: ellipsis; overflow-y: hidden; + border-radius: 0; &:hover, &:focus { From ab2808531dcd768a03f4a9fd90ec0ca67d013278 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 11:37:54 -0600 Subject: [PATCH 065/185] Remove unnecessary dismissCurrentDropdown --- .../filtered_search/filtered_search_manager.js.es6 | 7 ------- 1 file changed, 7 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 7e6144b571d..c509a3c3b62 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -154,7 +154,6 @@ const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding; - this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; if (!dropdownHint) { @@ -165,12 +164,6 @@ } } - dismissCurrentDropdown() { - if (this.currentDropdown === 'hint') { - dropdownHint.destroy(); - } - } - setDropdown() { const { lastToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); From 60c9240bc27ea1c5de1acb2a4cc686d9e2a85555 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:46:16 -0600 Subject: [PATCH 066/185] Fixed bug where labels with multiple spaces wouldn't get tokenized correctly --- .../filtered_search/filtered_search_tokenizer.es6 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index b686a43cf32..17fdfe0f550 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -40,18 +40,21 @@ let lastQuotation = ''; let incompleteToken = false; + // Iterate through each word (broken up by spaces) inputs.forEach((i) => { if (incompleteToken) { + // Continue previous token as it had an escaped + // quote in the beginning const prevToken = tokens.last(); prevToken.value += ` ${i}`; - // Remove last quotation + // Remove last quotation from the value const lastQuotationRegex = new RegExp(lastQuotation, 'g'); prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); tokens[tokens.length - 1] = prevToken; // Check to see if this quotation completes the token value - if (i.indexOf(lastQuotation)) { + if (i.indexOf(lastQuotation) !== -1) { lastToken = tokens.last(); incompleteToken = !incompleteToken; } From da8ab2bc086b6b7c2775d44198a6b3bc04794c3d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:52:23 -0600 Subject: [PATCH 067/185] Add escape quotations for selected labels from dropdown --- .../filtered_search/dropdown_label.js.es6 | 18 ++++++++++++++++-- .../filtered_search_manager.js.es6 | 7 ++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index ef92ecd3bd1..cd1ccb541e6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -12,8 +12,22 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - const labelName = `~${e.detail.selected.querySelector('.label-title').innerText.trim()}`; - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(labelName)); + let labelTitle = e.detail.selected.querySelector('.label-title').innerText.trim(); + + // Encapsulate label with quotes if it has spaces + if (labelTitle.indexOf(' ') !== -1) { + if (labelTitle.indexOf('"') !== -1) { + // Use single quotes if label title contains double quotes + labelTitle = `'${labelTitle}'`; + } else { + // Known side effect: Label's with both single and double quotes + // won't escape properly + labelTitle = `"${labelTitle}"`; + } + } + + const labelName = `~${labelTitle}`; + gl.FilteredSearchManager.addWordToInput(labelName); } this.dismissDropdown(); 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 c509a3c3b62..04374525d4c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -97,7 +97,12 @@ const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); if (lastToken.hasOwnProperty('key')) { - document.querySelector('.filtered-search').value = filteredSearchValue.slice(0, -1 * (lastToken.value.length)); + console.log(lastToken); + // Spaces inside the token means that the token value will be escaped by quotes + const hasQuotes = lastToken.value.indexOf(' ') !== -1; + + const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; + document.querySelector('.filtered-search').value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); } document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; From 5cd90ef9489797c858bfb7eec7be0f5773d4d417 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 12:57:07 -0600 Subject: [PATCH 068/185] Add escaping for milestone values --- .../filtered_search/dropdown_label.js.es6 | 17 ++--------------- .../filtered_search/dropdown_milestone.js.es6 | 3 ++- .../filtered_search_dropdown.js.es6 | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index cd1ccb541e6..d4a50422c3b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -12,21 +12,8 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - let labelTitle = e.detail.selected.querySelector('.label-title').innerText.trim(); - - // Encapsulate label with quotes if it has spaces - if (labelTitle.indexOf(' ') !== -1) { - if (labelTitle.indexOf('"') !== -1) { - // Use single quotes if label title contains double quotes - labelTitle = `'${labelTitle}'`; - } else { - // Known side effect: Label's with both single and double quotes - // won't escape properly - labelTitle = `"${labelTitle}"`; - } - } - - const labelName = `~${labelTitle}`; + const labelTitle = e.detail.selected.querySelector('.label-title').innerText.trim(); + const labelName = `~${this.getEscapedText(labelTitle)}`; gl.FilteredSearchManager.addWordToInput(labelName); } diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 8c75bd30e97..965a8c8a58d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -12,7 +12,8 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - const milestoneName = `%${e.detail.selected.querySelector('.btn-link').innerText.trim()}`; + const milestoneTitle = e.detail.selected.querySelector('.btn-link').innerText.trim(); + const milestoneName = `%${this.getEscapedText(milestoneTitle)}`; gl.FilteredSearchManager.addWordToInput(this.getSelectedText(milestoneName)); } 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 a345b368238..cc7f61c23e5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -18,6 +18,24 @@ this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); } + getEscapedText(text) { + let escapedText = text; + + // Encapsulate value with quotes if it has spaces + if (text.indexOf(' ') !== -1) { + if (text.indexOf('"') !== -1) { + // Use single quotes if value contains double quotes + escapedText = `'${text}'`; + } else { + // Known side effect: values's with both single and double quotes + // won't escape properly + escapedText = `"${text}"`; + } + } + + return escapedText; + } + getSelectedText(selectedToken) { // TODO: Get last word from FilteredSearchTokenizer const lastWord = this.input.value.split(' ').last(); From 00ed5aafd227a7d4f9be6448f1bcdacff6ee6dfa Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 15:13:31 -0600 Subject: [PATCH 069/185] Make keep typing dropdown item static --- .../filtered_search/dropdown_hint.js.es6 | 4 ---- .../shared/issuable/_search_bar.html.haml | 23 ++++++++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index b7161d00eb9..b09136586c8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -3,10 +3,6 @@ ((global) => { const dropdownData = [{ - icon: 'fa-search', - hint: 'Keep typing and press Enter', - tag: '', - },{ icon: 'fa-pencil', hint: 'author:', tag: '<author>' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 39af0c2c288..0a5de59cb63 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -16,14 +16,21 @@ = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') - %ul#js-dropdown-hint.dropdown-menu.hint-dropdown{ 'data-dynamic' => true } - %li.filter-dropdown-item - %button.btn.btn-link - %i.fa{ 'class': '{{icon}}'} - %span.js-filter-hint - {{hint}} - %span.js-filter-tag.dropdown-light-content - {{tag}} + #js-dropdown-hint.dropdown-menu.hint-dropdown + %ul + %li.filter-dropdown-item{ 'data-value': 'none' } + %button.btn.btn-link + = icon('search') + %span + Keep typing and press Enter + %ul.filter-dropdown{ 'data-dynamic' => true } + %li.filter-dropdown-item + %button.btn.btn-link + %i.fa{ 'class': '{{icon}}'} + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } %ul.filter-dropdown{ 'data-dynamic' => true } %li.filter-dropdown-item From d91d586aa60c8194a675e936832667cfe12729be Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:12:00 -0600 Subject: [PATCH 070/185] Add filter by last token --- .../javascripts/droplab/droplab_filter.js | 13 ++++- .../filtered_search/dropdown_author.js.es6 | 8 +++ .../filtered_search_dropdown.js.es6 | 13 +++-- .../filtered_search_tokenizer.es6 | 49 +++++++++++++++++++ 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js index 88e69c02422..5ae81afaf89 100644 --- a/app/assets/javascripts/droplab/droplab_filter.js +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -10,13 +10,22 @@ droplab.plugin(function init(DropLab) { var matches = []; // will only work on dynamically set data // and if a config text property is set - if(!data || !config.hasOwnProperty('text')){ + if(!data || (!config.hasOwnProperty('text') && !config.hasOwnProperty('filter'))){ return; } - matches = data.map(function(o){ + + var filterFunction = function(o){ // cheap string search o.droplab_hidden = o[config.text].toLowerCase().indexOf(value) === -1; return o; + }; + + if (config.hasOwnProperty('filter') && config.filter !== undefined) { + filterFunction = config.filter; + } + + matches = data.map(function(o) { + return filterFunction(o, value); }); list.render(matches); } diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 75eb1c06fbd..64c310ba7ad 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -19,6 +19,14 @@ super.renderContent(); droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutPrefix = value.slice(1); + + item.droplab_hidden = item['username'].indexOf(valueWithoutPrefix) === -1; + return item; + } } global.DropdownAuthor = DropdownAuthor; 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 cc7f61c23e5..bbfe26e6a21 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -50,12 +50,17 @@ getFilterConfig(filterKeyword) { const config = {}; - const filterConfig = { - text: filterKeyword, - }; + const filterConfig = {}; - config[this.hookId] = filterKeyword ? filterConfig : {}; + if (filterKeyword) { + filterConfig.text = filterKeyword; + } + if (this.filterMethod) { + filterConfig.filter = this.filterMethod; + } + + config[this.hookId] = filterConfig; return config; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index 17fdfe0f550..d6df83a3fb9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -29,6 +29,55 @@ } } + static getLastTokenObject(input) { + const token = FilteredSearchTokenizer.getLastToken(input); + const colonIndex = token.indexOf(':'); + + const key = colonIndex !== -1 ? token.slice(0, colonIndex) : ''; + const value = colonIndex !== -1 ? token.slice(colonIndex) : token; + + return { + key, + value, + } + } + + static getLastToken(input) { + let completeToken = false; + let completeQuotation = true; + let lastQuotation = ''; + let i = input.length; + + const doubleQuote = '"'; + const singleQuote = '\''; + while(!completeToken && i >= 0) { + const isDoubleQuote = input[i] === doubleQuote; + const isSingleQuote = input[i] === singleQuote; + + // If the second quotation is found + if ((lastQuotation === doubleQuote && input[i] === doubleQuote) || + (lastQuotation === singleQuote && input[i] === singleQuote)) { + completeQuotation = true; + } + + // Save the first quotation + if ((input[i] === doubleQuote && lastQuotation === '') || + (input[i] === singleQuote && lastQuotation === '')) { + lastQuotation = input[i]; + completeQuotation = false; + } + + if (completeQuotation && input[i] === ' ') { + completeToken = true; + } else { + i--; + } + } + + // Adjust by 1 because of empty space + return input.slice(i + 1); + } + static processTokens(input) { let tokens = []; let searchToken = ''; From 6d2d2b2bd1448bb46b586d5dbe0edc88f020967c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:20:14 -0600 Subject: [PATCH 071/185] Add filtering to the remaining dropdowns --- .../javascripts/filtered_search/dropdown_label.js.es6 | 8 ++++++++ .../javascripts/filtered_search/dropdown_milestone.js.es6 | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index d4a50422c3b..c5493f7a887 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -24,6 +24,14 @@ super.renderContent(); droplab.setData(this.hookId, 'labels.json'); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutPrefix = value.slice(1); + + item.droplab_hidden = item['title'].indexOf(valueWithoutPrefix) === -1; + return item; + } } global.DropdownLabel = DropdownLabel; diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 965a8c8a58d..8317ce5824c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -24,6 +24,14 @@ super.renderContent(); droplab.setData(this.hookId, 'milestones.json'); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutPrefix = value.slice(1); + + item.droplab_hidden = item['title'].indexOf(valueWithoutPrefix) === -1; + return item; + } } global.DropdownMilestone = DropdownMilestone; From b16a38c8a2c9b1090e3b28c47516e9161130ee10 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 6 Dec 2016 16:23:04 -0600 Subject: [PATCH 072/185] Add filterMethod to hint dropdown --- .../javascripts/filtered_search/dropdown_hint.js.es6 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index b09136586c8..0bee2eb2986 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -42,6 +42,18 @@ super.renderContent(); droplab.setData(this.hookId, dropdownData); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + + if (value === '') { + item.droplab_hidden = false; + } else { + item.droplab_hidden = item['hint'].indexOf(value) === -1; + } + + return item; + } } global.DropdownHint = DropdownHint; From 6d2a8b5b140a26f5e83ae7b6497cf14bc9a065ee Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:33:02 -0600 Subject: [PATCH 073/185] Add font to dropdown offset calculation --- .../filtered_search/filtered_search_manager.js.es6 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 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 04374525d4c..7e399427cef 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -108,18 +108,22 @@ document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; } - loadDropdown(dropdownName = '') { + loadDropdown(dropdownName = '', hideDropdown) { dropdownName = dropdownName.toLowerCase(); const filterIconPadding = 27; const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; const filteredSearch = document.querySelector('.filtered-search'); + if (!this.font) { + this.font = window.getComputedStyle(filteredSearch).font; + } + if (match && this.currentDropdown !== match.key) { console.log(`🦄 load ${match.key} dropdown`); const dynamicDropdownPadding = 12; - const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding + dynamicDropdownPadding; + const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding + dynamicDropdownPadding; this.dismissCurrentDropdown(); this.currentDropdown = match.key; @@ -157,8 +161,9 @@ } else if (!match && this.currentDropdown !== 'hint') { console.log('🦄 load hint dropdown'); - const dropdownOffset = gl.text.getTextWidth(filteredSearch.value) + filterIconPadding; - + const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding; + console.log(dropdownOffset) + this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; if (!dropdownHint) { From 158e90d13527897b2bdd49733c537898a387f2cb Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:33:33 -0600 Subject: [PATCH 074/185] Add padding for clear button --- app/assets/stylesheets/framework/filters.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 0882af57482..205cecb4906 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -36,6 +36,7 @@ .form-control { padding-left: 25px; + padding-right: 25px; &:focus ~ .fa-filter { color: #444; From 15454eb503bda8ffa9579e08ebe3f3f7114b7643 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:51:25 -0600 Subject: [PATCH 075/185] Add ability to search for filter dropdowns without filter symbol --- .../filtered_search/dropdown_assignee.js.es6 | 15 +++++++++++++++ .../filtered_search/dropdown_author.js.es6 | 11 +++++++++-- .../filtered_search/dropdown_label.js.es6 | 8 ++++++-- .../filtered_search/dropdown_milestone.js.es6 | 9 +++++++-- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index e791de5ad41..63fbe30ee84 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -23,6 +23,21 @@ super.renderContent(); droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); } + + filterMethod(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutPrefix = valueWithoutColon.slice(1); + + const username = item.username.toLowerCase(); + const name = item.name.toLowerCase(); + + const noUsernameMatch = username.indexOf(valueWithoutPrefix) === -1 && username.indexOf(valueWithoutColon) === -1; + const noNameMatch = name.indexOf(valueWithoutColon) === -1; + + item.droplab_hidden = noUsernameMatch && noNameMatch; + return item; + } } global.DropdownAssignee = DropdownAssignee; diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 64c310ba7ad..37e2e80533b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -22,9 +22,16 @@ filterMethod(item, query) { const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutPrefix = value.slice(1); + const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutPrefix = valueWithoutColon.slice(1); - item.droplab_hidden = item['username'].indexOf(valueWithoutPrefix) === -1; + const username = item.username.toLowerCase(); + const name = item.name.toLowerCase(); + + const noUsernameMatch = username.indexOf(valueWithoutPrefix) === -1 && username.indexOf(valueWithoutColon) === -1; + const noNameMatch = name.indexOf(valueWithoutColon) === -1; + + item.droplab_hidden = noUsernameMatch && noNameMatch; return item; } } diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index c5493f7a887..e2c1305597a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -27,9 +27,13 @@ filterMethod(item, query) { const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutPrefix = value.slice(1); + const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutPrefix = valueWithoutColon.slice(1); - item.droplab_hidden = item['title'].indexOf(valueWithoutPrefix) === -1; + const title = item.title.toLowerCase(); + const noTitleMatch = title.indexOf(valueWithoutPrefix) === -1 && title.indexOf(valueWithoutColon) === -1; + + item.droplab_hidden = noTitleMatch; return item; } } diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 8317ce5824c..cd185d31917 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -27,9 +27,14 @@ filterMethod(item, query) { const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutPrefix = value.slice(1); + const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutPrefix = valueWithoutColon.slice(1); - item.droplab_hidden = item['title'].indexOf(valueWithoutPrefix) === -1; + const title = item.title.toLowerCase(); + + const noTitleMatch = title.indexOf(valueWithoutPrefix) === -1 && title.indexOf(valueWithoutColon) === -1; + + item.droplab_hidden = noTitleMatch; return item; } } From bbad61b97c483a2d4e2a153c2e1b10d21edaa1e0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:54:29 -0600 Subject: [PATCH 076/185] Fix Droplab --- app/assets/javascripts/droplab/droplab.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 6befa0976d4..84cd89297ff 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -212,7 +212,8 @@ require('./window')(function(w){ var self = this; window.addEventListener('click', function(e){ var thisTag = e.target; - if(thisTag.tagName === 'LI' || thisTag.tagName === 'A'){ + if(thisTag.tagName === 'LI' || thisTag.tagName === 'A' + || thisTag.tagName === 'BUTTON'){ // climb up the tree to find the UL thisTag = utils.closest(thisTag, 'UL'); } @@ -556,7 +557,7 @@ var camelize = function(str) { }; var closest = function(thisTag, stopTag) { - while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + while(thisTag !== null && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ thisTag = thisTag.parentNode; } return thisTag; From ceb79e3c3cb595ac851fb32c99264c1f43dd9c75 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 7 Dec 2016 12:55:03 -0600 Subject: [PATCH 077/185] Reset filters after clear search --- .../filtered_search_dropdown.js.es6 | 38 ++++++++++++++++++- .../filtered_search_manager.js.es6 | 29 ++++++++------ 2 files changed, 55 insertions(+), 12 deletions(-) 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 bbfe26e6a21..edffd7fb8e2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -71,6 +71,20 @@ this.unbindEvents(); } + show() { + const currentHook = this.getCurrentHook(); + if (currentHook) { + currentHook.list.show(); + } + } + + hide() { + const currentHook = this.getCurrentHook(); + if (currentHook) { + currentHook.list.hide(); + } + } + dismissDropdown() { this.input.focus(); // Propogate input change to FilteredSearchManager @@ -104,7 +118,7 @@ droplab.setConfig(this.getFilterConfig(this.filterKeyword)); } - render() { + render(hide) { this.setAsDropdown(); const firstTimeInitialized = this.getCurrentHook() === undefined; @@ -115,6 +129,28 @@ droplab.changeHookList(this.hookId, `#${this.listId}`); this.renderContent(); } + + if (hide) { + this.hide(); + } else { + this.show(); + } + } + + resetFilters() { + const currentHook = this.getCurrentHook(); + + if (currentHook) { + const list = currentHook.list; + + if (list.data) { + const data = list.data.map((item) => { + item.droplab_hidden = false; + }); + + list.render(data); + } + } } } 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 7e399427cef..841738ff627 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,13 +1,5 @@ /* eslint-disable no-param-reassign */ ((global) => { - function clearSearch(e) { - e.stopPropagation(); - e.preventDefault(); - - document.querySelector('.filtered-search').value = ''; - document.querySelector('.clear-search').classList.add('hidden'); - } - function toggleClearSearchButton(e) { const clearSearchButton = document.querySelector('.clear-search'); @@ -170,7 +162,13 @@ dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), filteredSearch, this.currentDropdown); } dropdownHint.setOffset(dropdownOffset); - dropdownHint.render(); + dropdownHint.render(hideDropdown); + } + } + + dismissCurrentDropdown() { + if (this.currentDropdown === 'hint') { + dropdownHint.destroy(); } } @@ -198,7 +196,17 @@ filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); filteredSearchInput.addEventListener('input', toggleClearSearchButton); filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - document.querySelector('.clear-search').addEventListener('click', clearSearch); + document.querySelector('.clear-search').addEventListener('click', this.clearSearch.bind(this)); + } + + clearSearch(e) { + e.stopPropagation(); + e.preventDefault(); + + document.querySelector('.filtered-search').value = ''; + document.querySelector('.clear-search').classList.add('hidden'); + dropdownHint.resetFilters(); + this.loadDropdown('hint', true); } checkDropdownToken(e) { @@ -208,7 +216,6 @@ // Check for dropdown token if (lastToken[lastToken.length - 1] === ':') { const token = lastToken.slice(0, -1); - } } From 367a15882a07a2f48dd7887fea642baf3920b6b7 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 15:36:54 -0600 Subject: [PATCH 078/185] Update droplab --- app/assets/javascripts/droplab/droplab.js | 192 ++++++++++++------ .../javascripts/droplab/droplab_ajax.js | 89 ++++---- .../javascripts/droplab/droplab_filter.js | 81 +++++--- .../filtered_search/dropdown_assignee.js.es6 | 13 +- .../filtered_search/dropdown_author.js.es6 | 13 +- .../filtered_search/dropdown_hint.js.es6 | 22 +- .../filtered_search/dropdown_label.js.es6 | 30 ++- .../filtered_search/dropdown_milestone.js.es6 | 30 ++- .../filtered_search_dropdown.js.es6 | 60 +++--- .../filtered_search_manager.js.es6 | 42 ++-- 10 files changed, 357 insertions(+), 215 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 84cd89297ff..4d83b609a73 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -53,6 +53,7 @@ Object.assign(DropDown.prototype, { this.list.addEventListener('click', function(e) { // climb up the tree to find the LI var selected = utils.closest(e.target, 'LI'); + if(selected) { e.preventDefault(); self.hide(); @@ -158,17 +159,22 @@ require('./window')(function(w){ this.ready = false; this.hooks = []; this.queuedData = []; - this.plugins = []; this.config = {}; + this.loadWrapper; if(typeof hook !== 'undefined'){ this.addHook(hook); } - this.addEvents(); }; + Object.assign(DropLab.prototype, { - plugin: function (plugin) { - this.plugins.push(plugin) + load: function() { + this.loadWrapper(); + }, + + loadWrapper: function(){ + var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']')); + this.addHooks(dropdownTriggers).init(); }, addData: function () { @@ -181,6 +187,14 @@ require('./window')(function(w){ this.applyArgs(args, '_setData'); }, + destroy: function() { + this.hooks.forEach(function(h){ + h.destroy(); + }); + this.hooks = []; + this.removeEvents(); + }, + applyArgs: function(args, methodName) { if(this.ready) { this[methodName].apply(this, args); @@ -210,7 +224,7 @@ require('./window')(function(w){ addEvents: function() { var self = this; - window.addEventListener('click', function(e){ + this.windowClickedWrapper = function(e){ var thisTag = e.target; if(thisTag.tagName === 'LI' || thisTag.tagName === 'A' || thisTag.tagName === 'BUTTON'){ @@ -222,10 +236,16 @@ require('./window')(function(w){ self.hooks.forEach(function(hook) { hook.list.hide(); }); - }); + }.bind(this); + w.addEventListener('click', this.windowClickedWrapper); }, - changeHookList: function(trigger, list) { + removeEvents: function(){ + w.removeEventListener('click', this.windowClickedWrapper); + w.removeEventListener('load', this.loadWrapper); + }, + + changeHookList: function(trigger, list, plugins, config) { trigger = document.querySelector('[data-id="'+trigger+'"]'); list = document.querySelector(list); this.hooks.every(function(hook, i) { @@ -234,19 +254,16 @@ require('./window')(function(w){ hook.list.list.innerHTML = hook.list.initialState; hook.list.hide(); - hook.trigger.removeEventListener('mousedown', hook.events.mousedown); - hook.trigger.removeEventListener('input', hook.events.input); - hook.trigger.removeEventListener('keyup', hook.events.keyup); - hook.trigger.removeEventListener('keydown', hook.events.keydown); + hook.destroy(); this.hooks.splice(i, 1); - this.addHook(trigger, list); + this.addHook(trigger, list, plugins, config); return false; } return true }.bind(this)); }, - addHook: function(hook, list) { + addHook: function(hook, list, plugins, config) { if(!(hook instanceof HTMLElement) && typeof hook === 'string'){ hook = document.querySelector(hook); } @@ -256,17 +273,17 @@ require('./window')(function(w){ if(hook) { if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { - this.hooks.push(new HookButton(hook, list)); + this.hooks.push(new HookButton(hook, list, plugins, config)); } else if(hook.tagName === 'INPUT') { - this.hooks.push(new HookInput(hook, list)); + this.hooks.push(new HookInput(hook, list, plugins, config)); } } return this; }, - addHooks: function(hooks) { + addHooks: function(hooks, plugins, config) { hooks.forEach(function(hook) { - this.addHook(hook, null); + this.addHook(hook, null, plugins, config); }.bind(this)); return this; }, @@ -276,9 +293,7 @@ require('./window')(function(w){ }, init: function () { - this.plugins.forEach(function(plugin) { - plugin(DropLab); - }) + this.addEvents(); var readyEvent = new CustomEvent('ready.dl', { detail: { dropdown: this, @@ -301,15 +316,18 @@ require('./window')(function(w){ },{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){ var DropDown = require('./dropdown'); -var Hook = function(trigger, list){ +var Hook = function(trigger, list, plugins, config){ this.trigger = trigger; this.list = new DropDown(list); this.type = 'Hook'; this.event = 'click'; + this.plugins = plugins || []; + this.config = config || {}; this.id = trigger.dataset.id; }; Object.assign(Hook.prototype, { + addEvents: function(){}, constructor: Hook, @@ -321,31 +339,61 @@ module.exports = Hook; var CustomEvent = require('./custom_event_polyfill'); var Hook = require('./hook'); -var HookButton = function(trigger, list) { - Hook.call(this, trigger, list); +var HookButton = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); this.type = 'button'; this.event = 'click'; this.addEvents(); + this.addPlugins(); }; HookButton.prototype = Object.create(Hook.prototype); Object.assign(HookButton.prototype, { - addEvents: function(){ - var self = this; - this.trigger.addEventListener('click', function(e){ - var buttonEvent = new CustomEvent('click.dl', { - detail: { - hook: self, - }, - bubbles: true, - cancelable: true - }); - self.list.show(); - e.target.dispatchEvent(buttonEvent); + addPlugins: function() { + this.plugins.forEach(function(plugin) { + plugin.init(this); }); }, + clicked: function(e){ + var buttonEvent = new CustomEvent('click.dl', { + detail: { + hook: this, + }, + bubbles: true, + cancelable: true + }); + this.list.show(); + e.target.dispatchEvent(buttonEvent); + }, + + addEvents: function(){ + this.clickedWrapper = this.clicked.bind(this); + this.trigger.addEventListener('click', this.clickedWrapper); + }, + + removeEvents: function(){ + this.trigger.removeEventListener('click', this.clickedWrapper); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + this.plugins.forEach(function(plugin) { + plugin.destroy(); + }); + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + }, + + constructor: HookButton, }); @@ -356,18 +404,26 @@ module.exports = HookButton; var CustomEvent = require('./custom_event_polyfill'); var Hook = require('./hook'); -var HookInput = function(trigger, list) { - Hook.call(this, trigger, list); +var HookInput = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); this.type = 'input'; this.event = 'input'; + this.addPlugins(); this.addEvents(); }; Object.assign(HookInput.prototype, { + addPlugins: function() { + var self = this; + this.plugins.forEach(function(plugin) { + plugin.init(self); + }); + }, + addEvents: function(){ var self = this; - function mousedown(e) { + this.mousedown = function mousedown(e) { var mouseEvent = new CustomEvent('mousedown.dl', { detail: { hook: self, @@ -379,7 +435,7 @@ Object.assign(HookInput.prototype, { e.target.dispatchEvent(mouseEvent); } - function input(e) { + this.input = function input(e) { var inputEvent = new CustomEvent('input.dl', { detail: { hook: self, @@ -392,11 +448,11 @@ Object.assign(HookInput.prototype, { self.list.show(); } - function keyup(e) { + this.keyup = function keyup(e) { keyEvent(e, 'keyup.dl'); } - function keydown(e) { + this.keydown = function keydown(e) { keyEvent(e, 'keydown.dl'); } @@ -416,15 +472,38 @@ Object.assign(HookInput.prototype, { } this.events = this.events || {}; - this.events.mousedown = mousedown; - this.events.input = input; - this.events.keyup = keyup; - this.events.keydown = keydown; - this.trigger.addEventListener('mousedown', mousedown); - this.trigger.addEventListener('input', input); - this.trigger.addEventListener('keyup', keyup); - this.trigger.addEventListener('keydown', keydown); + this.events.mousedown = this.mousedown; + this.events.input = this.input; + this.events.keyup = this.keyup; + this.events.keydown = this.keydown; + this.trigger.addEventListener('mousedown', this.mousedown); + this.trigger.addEventListener('input', this.input); + this.trigger.addEventListener('keyup', this.keyup); + this.trigger.addEventListener('keydown', this.keydown); }, + + removeEvents: function(){ + this.trigger.removeEventListener('mousedown', this.mousedown); + this.trigger.removeEventListener('input', this.input); + this.trigger.removeEventListener('keyup', this.keyup); + this.trigger.removeEventListener('keydown', this.keydown); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + this.plugins.forEach(function(plugin) { + plugin.destroy(); + }); + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + } }); module.exports = HookInput; @@ -433,21 +512,14 @@ module.exports = HookInput; var DropLab = require('./droplab')(); var DATA_TRIGGER = require('./constants').DATA_TRIGGER; var keyboard = require('./keyboard')(); - var setup = function() { - var droplab = DropLab(); - require('./window')(function(w) { - w.addEventListener('load', function() { - var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']')); - droplab.addHooks(dropdownTriggers).init(); - }); - }); - return droplab; + window.DropLab = DropLab; }; + module.exports = setup(); -},{"./constants":1,"./droplab":4,"./keyboard":9,"./window":11}],9:[function(require,module,exports){ +},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){ require('./window')(function(w){ module.exports = function(){ var currentKey; @@ -557,7 +629,7 @@ var camelize = function(str) { }; var closest = function(thisTag, stopTag) { - while(thisTag !== null && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + while(thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ thisTag = thisTag.parentNode; } return thisTag; diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 2dff5b83fae..b81663c281d 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -1,52 +1,59 @@ /* eslint-disable */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { class DropdownAssignee extends gl.FilteredSearchDropdown { - constructor(dropdown, input) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-assignee'; } @@ -20,8 +20,13 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + // TODO: Pass elements instead of querySelectors + this.droplab.changeHookList(this.hookId, '#js-dropdown-assignee', [droplabAjax], { + droplabAjax: { + endpoint: '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users=', + method: 'setData', + } + }); } filterMethod(item, query) { diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 37e2e80533b..c02f1e25407 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -3,8 +3,8 @@ ((global) => { class DropdownAuthor extends gl.FilteredSearchDropdown { - constructor(dropdown, input) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-author'; } @@ -16,8 +16,13 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users='); + // TODO: Pass elements instead of querySelectors + this.droplab.changeHookList(this.hookId, '#js-dropdown-author', [droplabAjax], { + droplabAjax: { + endpoint: '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users=', + method: 'setData', + } + }); } filterMethod(item, query) { diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 0bee2eb2986..481faa7fd49 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -21,10 +21,9 @@ }]; class DropdownHint extends gl.FilteredSearchDropdown { - constructor(dropdown, input, filterKeyword) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-hint'; - this.filterKeyword = filterKeyword; } itemClicked(e) { @@ -39,8 +38,13 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, dropdownData); + this.droplab.changeHookList(this.hookId, '#js-dropdown-hint', [droplabFilter], { + droplabFilter: { + template: 'hint', + filterFunction: this.filterMethod, + } + }); + this.droplab.setData(this.hookId, dropdownData); } filterMethod(item, query) { @@ -54,6 +58,14 @@ return item; } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], { + droplabFilter: { + template: 'hint', + } + }).init(); + } } global.DropdownHint = DropdownHint; diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index e2c1305597a..af47ad2a1f8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -3,9 +3,10 @@ ((global) => { class DropdownLabel extends gl.FilteredSearchDropdown { - constructor(dropdown, input) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-label'; + this.filterSymbol = '~'; } itemClicked(e) { @@ -21,20 +22,17 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, 'labels.json'); - } - - filterMethod(item, query) { - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const valueWithoutPrefix = valueWithoutColon.slice(1); - - const title = item.title.toLowerCase(); - const noTitleMatch = title.indexOf(valueWithoutPrefix) === -1 && title.indexOf(valueWithoutColon) === -1; - - item.droplab_hidden = noTitleMatch; - return item; + // TODO: Pass elements instead of querySelectors + // TODO: Don't bind filterWithSymbol to (this), just pass the symbol + this.droplab.changeHookList(this.hookId, '#js-dropdown-label', [droplabAjax, droplabFilter], { + droplabAjax: { + endpoint: 'labels.json', + method: 'setData', + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this), + } + }); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index cd185d31917..9810767eb66 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -3,9 +3,10 @@ ((global) => { class DropdownMilestone extends gl.FilteredSearchDropdown { - constructor(dropdown, input) { - super(dropdown, input); + constructor(droplab, dropdown, input) { + super(droplab, dropdown, input); this.listId = 'js-dropdown-milestone'; + this.filterSymbol = '%'; } itemClicked(e) { @@ -21,21 +22,16 @@ } renderContent() { - super.renderContent(); - droplab.setData(this.hookId, 'milestones.json'); - } - - filterMethod(item, query) { - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const valueWithoutPrefix = valueWithoutColon.slice(1); - - const title = item.title.toLowerCase(); - - const noTitleMatch = title.indexOf(valueWithoutPrefix) === -1 && title.indexOf(valueWithoutColon) === -1; - - item.droplab_hidden = noTitleMatch; - return item; + // TODO: Pass elements instead of querySelectors + this.droplab.changeHookList(this.hookId, '#js-dropdown-milestone', [droplabAjax, droplabFilter], { + droplabAjax: { + endpoint: 'milestones.json', + method: 'setData', + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this), + } + }); } } 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 edffd7fb8e2..80c3407b7fa 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -3,7 +3,8 @@ const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; class FilteredSearchDropdown { - constructor(dropdown, input) { + constructor(droplab, dropdown, input) { + this.droplab = droplab; this.hookId = 'filtered-search'; this.input = input; this.dropdown = dropdown; @@ -66,25 +67,11 @@ destroy() { this.input.setAttribute(DATA_DROPDOWN_TRIGGER, ''); - droplab.setConfig(this.getFilterConfig()); - droplab.setData(this.hookId, []); + this.droplab.setConfig(this.getFilterConfig()); + this.droplab.setData(this.hookId, []); this.unbindEvents(); } - show() { - const currentHook = this.getCurrentHook(); - if (currentHook) { - currentHook.list.show(); - } - } - - hide() { - const currentHook = this.getCurrentHook(); - if (currentHook) { - currentHook.list.hide(); - } - } - dismissDropdown() { this.input.focus(); // Propogate input change to FilteredSearchManager @@ -111,30 +98,24 @@ } getCurrentHook() { - return droplab.hooks.filter(h => h.id === this.hookId)[0]; + return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } renderContent() { - droplab.setConfig(this.getFilterConfig(this.filterKeyword)); + // Overriden by dropdown sub class } - render(hide) { + render(forceRenderContent) { this.setAsDropdown(); const firstTimeInitialized = this.getCurrentHook() === undefined; - if (firstTimeInitialized) { + if (firstTimeInitialized || forceRenderContent) { this.renderContent(); } else if(this.getCurrentHook().list.list.id !== this.listId) { - droplab.changeHookList(this.hookId, `#${this.listId}`); + // this.droplab.changeHookList(this.hookId, `#${this.listId}`); this.renderContent(); } - - if (hide) { - this.hide(); - } else { - this.show(); - } } resetFilters() { @@ -152,6 +133,29 @@ } } } + + hide() { + const currentHook = this.getCurrentHook(); + if (currentHook) { + currentHook.list.hide(); + } + } + + filterWithSymbol(item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1).toLowerCase(); + const prefix = valueWithoutColon[0]; + const valueWithoutPrefix = valueWithoutColon.slice(1); + + const title = item.title.toLowerCase(); + + // Eg. this.filterSymbol = ~ for labels + const matchWithoutPrefix = prefix === this.filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + const match = title.indexOf(valueWithoutColon) !== -1; + + item.droplab_hidden = !match && !matchWithoutPrefix; + return item; + } } global.FilteredSearchDropdown = FilteredSearchDropdown; 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 841738ff627..af8e145fa7f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -101,11 +101,18 @@ } loadDropdown(dropdownName = '', hideDropdown) { + let firstLoad = false; + const filteredSearch = document.querySelector('.filtered-search'); + + if(!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + dropdownName = dropdownName.toLowerCase(); const filterIconPadding = 27; const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; - const filteredSearch = document.querySelector('.filtered-search'); if (!this.font) { this.font = window.getComputedStyle(filteredSearch).font; @@ -116,34 +123,38 @@ const dynamicDropdownPadding = 12; const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding + dynamicDropdownPadding; + const dropdownAuthorElement = document.querySelector('#js-dropdown-author'); + const dropdownAssigneeElement = document.querySelector('#js-dropdown-assignee'); + const dropdownMilestoneElement = document.querySelector('#js-dropdown-milestone'); + const dropdownLabelElemenet = document.querySelector('#js-dropdown-label'); this.dismissCurrentDropdown(); this.currentDropdown = match.key; if (match.key === 'author') { if (!dropdownAuthor) { - dropdownAuthor = new gl.DropdownAuthor(document.querySelector('#js-dropdown-author'), filteredSearch); + dropdownAuthor = new gl.DropdownAuthor(this.droplab, dropdownAuthorElement, filteredSearch); } dropdownAuthor.setOffset(dropdownOffset); dropdownAuthor.render(); } else if (match.key === 'assignee') { if (!dropdownAssignee) { - dropdownAssignee = new gl.DropdownAssignee(document.querySelector('#js-dropdown-assignee'), filteredSearch); + dropdownAssignee = new gl.DropdownAssignee(this.droplab, dropdownAssigneeElement, filteredSearch); } dropdownAssignee.setOffset(dropdownOffset); dropdownAssignee.render(); } else if (match.key === 'milestone') { if (!dropdownMilestone) { - dropdownMilestone = new gl.DropdownMilestone(document.querySelector('#js-dropdown-milestone'), filteredSearch); + dropdownMilestone = new gl.DropdownMilestone(this.droplab, dropdownMilestoneElement, filteredSearch); } dropdownMilestone.setOffset(dropdownOffset); dropdownMilestone.render(); } else if (match.key === 'label') { if (!dropdownLabel) { - dropdownLabel = new gl.DropdownLabel(document.querySelector('#js-dropdown-label'), filteredSearch); + dropdownLabel = new gl.DropdownLabel(this.droplab, dropdownLabelElemenet, filteredSearch); } dropdownLabel.setOffset(dropdownOffset); @@ -154,22 +165,29 @@ console.log('🦄 load hint dropdown'); const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding; - console.log(dropdownOffset) + const dropdownHintElement = document.querySelector('#js-dropdown-hint'); + this.dismissCurrentDropdown(); this.currentDropdown = 'hint'; - if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(document.querySelector('#js-dropdown-hint'), filteredSearch, this.currentDropdown); + dropdownHint = new gl.DropdownHint(this.droplab, dropdownHintElement, filteredSearch); } + + if (firstLoad) { + dropdownHint.configure(); + } + dropdownHint.setOffset(dropdownOffset); - dropdownHint.render(hideDropdown); + dropdownHint.render(firstLoad); } } dismissCurrentDropdown() { - if (this.currentDropdown === 'hint') { - dropdownHint.destroy(); - } + // if (this.currentDropdown === 'hint') { + // dropdownHint.hide(); + // } else if (this.currentDropdown === 'author') { + // // dropdownAuthor.hide(); + // } } setDropdown() { From 3cf0ee6c1f13d111da933cdea98b46582c7c4306 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 15:42:58 -0600 Subject: [PATCH 079/185] Fix turbolinks issue by cleaning up droplab on page:change --- .../filtered_search_manager.js.es6 | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 af8e145fa7f..f28ce6b4366 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -81,6 +81,25 @@ this.bindEvents(); loadSearchParamsFromURL(); this.setDropdown(); + + document.addEventListener('page:change', this.cleanup); + } + + cleanup() { + console.log('cleanup') + + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + dropdownHint = null; + dropdownAuthor = null; + dropdownAssignee = null; + dropdownMilestone = null; + dropdownLabel = null; + + document.removeEventListener('page:change', this.cleanup); } static addWordToInput(word, addSpace) { From c5029c65450522359ec8b869c1433ed89db3c303 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 8 Dec 2016 16:40:46 -0600 Subject: [PATCH 080/185] Add basic ajax filtering for author --- .../droplab/droplab_ajax_filter.js | 109 ++++++++++++++++++ .../filtered_search/dropdown_author.js.es6 | 34 +++--- 2 files changed, 129 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/droplab/droplab_ajax_filter.js diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js new file mode 100644 index 00000000000..b346f22f1c2 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -0,0 +1,109 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o -1; + if (invalidKeyPressed || this.loading) { + return; + } + + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.timeout = setTimeout(this.trigger.bind(this), 200); + }, + + trigger: function trigger() { + var config = this.hook.config.droplabAjaxFilter; + var searchValue = this.trigger.value; + + if (!config || !config.endpoint || !config.searchKey) { + return; + } + + if (config.searchValueFunction) { + searchValue = config.searchValueFunction(); + } + + if (searchValue === config.searchKey) { + return this.list.show(); + } + + this.loading = true; + this.hook.list.setData([]); + + var params = config.params || {}; + params[config.searchKey] = searchValue; + var self = this; + this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { + self.hook.list.addData.call(self.hook.list, data[0]); + self.notLoading(); + }); + }, + + _loadUrlData: function _loadUrlData(url) { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest; + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if(xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + return resolve([data, xhr]); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); + }, + + buildParams: function(params) { + if (!params) return ''; + var paramsArray = Object.keys(params).map(function(param) { + return param + '=' + (params[param] || ''); + }); + return '?' + paramsArray.join('&'); + }, + + destroy: function destroy() { + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTrigger); + this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +}); \ No newline at end of file diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index c02f1e25407..cb3a6b6ab6d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -17,27 +17,33 @@ renderContent() { // TODO: Pass elements instead of querySelectors - this.droplab.changeHookList(this.hookId, '#js-dropdown-author', [droplabAjax], { - droplabAjax: { - endpoint: '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users=', - method: 'setData', + this.droplab.changeHookList(this.hookId, '#js-dropdown-author', [droplabAjaxFilter], { + droplabAjaxFilter: { + endpoint: '/autocomplete/users.json', + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: 2, + current_user: true, + }, + searchValueFunction: this.getSearchInput, } }); } - filterMethod(item, query) { + getSearchInput() { + const query = document.querySelector('.filtered-search').value; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutColon = value.slice(1); + const hasPrefix = valueWithoutColon[0] === '@'; const valueWithoutPrefix = valueWithoutColon.slice(1); - const username = item.username.toLowerCase(); - const name = item.name.toLowerCase(); - - const noUsernameMatch = username.indexOf(valueWithoutPrefix) === -1 && username.indexOf(valueWithoutColon) === -1; - const noNameMatch = name.indexOf(valueWithoutColon) === -1; - - item.droplab_hidden = noUsernameMatch && noNameMatch; - return item; + if (hasPrefix) { + return valueWithoutPrefix; + } else { + return valueWithoutColon; + } } } From ed4e525a3bd05dfb32aa3c2baa9bf19688319b1a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 11:44:09 -0600 Subject: [PATCH 081/185] Code cleanup --- app/assets/javascripts/droplab/droplab.js | 11 +- .../droplab/droplab_ajax_filter.js | 13 +- .../filtered_search/dropdown_assignee.js.es6 | 41 ++-- .../filtered_search/dropdown_author.js.es6 | 31 +-- .../filtered_search/dropdown_hint.js.es6 | 30 +-- .../filtered_search/dropdown_label.js.es6 | 26 +-- .../filtered_search/dropdown_milestone.js.es6 | 25 ++- .../filtered_search_dropdown.js.es6 | 65 ++---- .../filtered_search_manager.js.es6 | 194 ++++++++---------- .../shared/issuable/_search_bar.html.haml | 2 +- 10 files changed, 203 insertions(+), 235 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 4d83b609a73..b17f156acb4 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -39,6 +39,10 @@ var DropDown = function(list, trigger) { this.getItems(); this.addEvents(); this.initialState = list.innerHTML; + + if (this.initialState.indexOf('{{') == -1) { + debugger + } }; Object.assign(DropDown.prototype, { @@ -138,6 +142,10 @@ Object.assign(DropDown.prototype, { this.list.style.display = 'none'; this.hidden = true; }, + + destroy: function() { + this.hide(); + } }); module.exports = DropDown; @@ -247,7 +255,7 @@ require('./window')(function(w){ changeHookList: function(trigger, list, plugins, config) { trigger = document.querySelector('[data-id="'+trigger+'"]'); - list = document.querySelector(list); + // list = document.querySelector(list); this.hooks.every(function(hook, i) { if(hook.trigger === trigger) { // Restore initial State @@ -503,6 +511,7 @@ Object.assign(HookInput.prototype, { this.restoreInitialState(); this.removeEvents(); this.removePlugins(); + this.list.destroy(); } }); diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index b346f22f1c2..c345fda1075 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -8,14 +8,12 @@ require('../window')(function(w){ this.hook = hook; this.notLoading(); - this.hook.trigger.addEventListener('keydown.dl', this.debounceTrigger.bind(this)); + this.debounceTriggerWrapper = this.debounceTrigger.bind(this); + this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper); + this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper); this.trigger(); }, - debounceTriggerWrapper() { - return this.debounceTrigger.bind(this.hook); - }, - notLoading: function notLoading() { this.loading = false; }, @@ -57,7 +55,8 @@ require('../window')(function(w){ params[config.searchKey] = searchValue; var self = this; this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { - self.hook.list.addData.call(self.hook.list, data[0]); + self.hook.restoreInitialState.call(self.hook); + self.hook.list.setData.call(self.hook.list, data[0]); self.notLoading(); }); }, @@ -93,7 +92,7 @@ require('../window')(function(w){ clearTimeout(this.timeout); } - this.hook.trigger.removeEventListener('keydown.dl', this.debounceTrigger); + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper); this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); } }; diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index 7609546a3a6..b2b03b637e7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -6,6 +6,19 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-assignee'; + this.config = { + droplabAjaxFilter: { + endpoint: '/autocomplete/users.json', + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: 2, + current_user: true, + }, + searchValueFunction: this.getSearchInput, + } + }; } itemClicked(e) { @@ -21,27 +34,25 @@ renderContent() { // TODO: Pass elements instead of querySelectors - this.droplab.changeHookList(this.hookId, '#js-dropdown-assignee', [droplabAjax], { - droplabAjax: { - endpoint: '/autocomplete/users.json?search=&per_page=20&active=true&project_id=2&group_id=&skip_ldap=&todo_filter=&todo_state_filter=¤t_user=true&push_code_to_protected_branches=&author_id=&skip_users=', - method: 'setData', - } - }); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); } - filterMethod(item, query) { + getSearchInput() { + const query = document.querySelector('.filtered-search').value; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); + const valueWithoutColon = value.slice(1); + const hasPrefix = valueWithoutColon[0] === '@'; const valueWithoutPrefix = valueWithoutColon.slice(1); - const username = item.username.toLowerCase(); - const name = item.name.toLowerCase(); + if (hasPrefix) { + return valueWithoutPrefix; + } else { + return valueWithoutColon; + } + } - const noUsernameMatch = username.indexOf(valueWithoutPrefix) === -1 && username.indexOf(valueWithoutColon) === -1; - const noNameMatch = name.indexOf(valueWithoutColon) === -1; - - item.droplab_hidden = noUsernameMatch && noNameMatch; - return item; + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index cb3a6b6ab6d..9bd49ab1a78 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -6,18 +6,7 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-author'; - } - - itemClicked(e) { - const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); - - this.dismissDropdown(); - } - - renderContent() { - // TODO: Pass elements instead of querySelectors - this.droplab.changeHookList(this.hookId, '#js-dropdown-author', [droplabAjaxFilter], { + this.config = { droplabAjaxFilter: { endpoint: '/autocomplete/users.json', searchKey: 'search', @@ -29,7 +18,19 @@ }, searchValueFunction: this.getSearchInput, } - }); + }; + } + + itemClicked(e) { + const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); + + this.dismissDropdown(); + } + + renderContent() { + // TODO: Pass elements instead of querySelectors + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); } getSearchInput() { @@ -45,6 +46,10 @@ return valueWithoutColon; } } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); + } } global.DropdownAuthor = DropdownAuthor; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 481faa7fd49..f885267880a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -24,26 +24,30 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-hint'; + this.config = { + droplabFilter: { + template: 'hint', + filterFunction: this.filterMethod, + } + }; } itemClicked(e) { - const token = e.detail.selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = e.detail.selected.querySelector('.js-filter-tag').innerText.trim(); + const selected = e.detail.selected; + if (!selected.hasAttribute('data-value')) { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); - if (tag.length) { - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); + if (tag.length) { + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); + } } this.dismissDropdown(); } renderContent() { - this.droplab.changeHookList(this.hookId, '#js-dropdown-hint', [droplabFilter], { - droplabFilter: { - template: 'hint', - filterFunction: this.filterMethod, - } - }); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); this.droplab.setData(this.hookId, dropdownData); } @@ -60,11 +64,7 @@ } configure() { - this.droplab.addHook(this.input, this.dropdown, [droplabFilter], { - droplabFilter: { - template: 'hint', - } - }).init(); + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index af47ad2a1f8..24a795808ca 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -6,7 +6,15 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-label'; - this.filterSymbol = '~'; + this.config = { + droplabAjax: { + endpoint: 'labels.json', + method: 'setData', + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this, '~'), + } + }; } itemClicked(e) { @@ -22,17 +30,11 @@ } renderContent() { - // TODO: Pass elements instead of querySelectors - // TODO: Don't bind filterWithSymbol to (this), just pass the symbol - this.droplab.changeHookList(this.hookId, '#js-dropdown-label', [droplabAjax, droplabFilter], { - droplabAjax: { - endpoint: 'labels.json', - method: 'setData', - }, - droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this), - } - }); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 9810767eb66..458a9b1c5c1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -6,7 +6,15 @@ constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.listId = 'js-dropdown-milestone'; - this.filterSymbol = '%'; + this.config = { + droplabAjax: { + endpoint: 'milestones.json', + method: 'setData', + }, + droplabFilter: { + filterFunction: this.filterWithSymbol.bind(this, '%'), + } + }; } itemClicked(e) { @@ -22,16 +30,11 @@ } renderContent() { - // TODO: Pass elements instead of querySelectors - this.droplab.changeHookList(this.hookId, '#js-dropdown-milestone', [droplabAjax, droplabFilter], { - droplabAjax: { - endpoint: 'milestones.json', - method: 'setData', - }, - droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this), - } - }); + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + } + + configure() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); } } 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 80c3407b7fa..2f92c7b2e2a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -12,11 +12,16 @@ } bindEvents() { - this.dropdown.addEventListener('click.dl', this.itemClicked.bind(this)); + this.itemClickedWrapper = this.itemClicked.bind(this); + this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); } unbindEvents() { - this.dropdown.removeEventListener('click.dl', this.itemClicked.bind(this)); + this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); + } + + getCurrentHook() { + return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } getEscapedText(text) { @@ -49,34 +54,8 @@ // Overridden by dropdown sub class } - getFilterConfig(filterKeyword) { - const config = {}; - const filterConfig = {}; - - if (filterKeyword) { - filterConfig.text = filterKeyword; - } - - if (this.filterMethod) { - filterConfig.filter = this.filterMethod; - } - - config[this.hookId] = filterConfig; - return config; - } - - destroy() { - this.input.setAttribute(DATA_DROPDOWN_TRIGGER, ''); - this.droplab.setConfig(this.getFilterConfig()); - this.droplab.setData(this.hookId, []); - this.unbindEvents(); - } - - dismissDropdown() { - this.input.focus(); - // Propogate input change to FilteredSearchManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new Event('input')); + renderContent() { + // Overriden by dropdown sub class } setAsDropdown() { @@ -97,13 +76,12 @@ return dataValue !== null; } - getCurrentHook() { - return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; - } - - renderContent() { - // Overriden by dropdown sub class - } + dismissDropdown() { + this.input.focus(); + // Propogate input change to FilteredSearchManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } render(forceRenderContent) { this.setAsDropdown(); @@ -134,14 +112,7 @@ } } - hide() { - const currentHook = this.getCurrentHook(); - if (currentHook) { - currentHook.list.hide(); - } - } - - filterWithSymbol(item, query) { + filterWithSymbol(filterSymbol, item, query) { const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); const valueWithoutColon = value.slice(1).toLowerCase(); const prefix = valueWithoutColon[0]; @@ -149,8 +120,8 @@ const title = item.title.toLowerCase(); - // Eg. this.filterSymbol = ~ for labels - const matchWithoutPrefix = prefix === this.filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + // Eg. filterSymbol = ~ for labels + const matchWithoutPrefix = prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; const match = title.indexOf(valueWithoutColon) !== -1; item.droplab_hidden = !match && !matchWithoutPrefix; 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 f28ce6b4366..9846f3ba50d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -69,20 +69,19 @@ } } - let dropdownHint; - let dropdownAuthor; - let dropdownAssignee; - let dropdownMilestone; - let dropdownLabel; - class FilteredSearchManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchInput = document.querySelector('.filtered-search'); + this.clearSearchButton = document.querySelector('.clear-search'); + + this.setupMapping(); this.bindEvents(); loadSearchParamsFromURL(); this.setDropdown(); - document.addEventListener('page:change', this.cleanup); + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); } cleanup() { @@ -93,124 +92,105 @@ this.droplab = null; } - dropdownHint = null; - dropdownAuthor = null; - dropdownAssignee = null; - dropdownMilestone = null; - dropdownLabel = null; + this.setupMapping(); - document.removeEventListener('page:change', this.cleanup); + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownAuthor', + element: document.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownAssignee', + element: document.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownMilestone', + element: document.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownLabel', + element: document.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: document.querySelector('#js-dropdown-hint'), + }, + } } static addWordToInput(word, addSpace) { - const filteredSearchValue = document.querySelector('.filtered-search').value; + const filteredSearchInput = document.querySelector('.filtered-search') + const filteredSearchValue = filteredSearchInput.value; const hasExistingValue = filteredSearchValue.length !== 0; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); + if (lastToken.hasOwnProperty('key')) { console.log(lastToken); // Spaces inside the token means that the token value will be escaped by quotes const hasQuotes = lastToken.value.indexOf(' ') !== -1; - const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; - document.querySelector('.filtered-search').value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); + filteredSearchInput.value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); } - document.querySelector('.filtered-search').value += hasExistingValue && addSpace ? ` ${word}` : word; + filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; } - loadDropdown(dropdownName = '', hideDropdown) { + load(key, firstLoad = false) { + console.log(`🦄 load ${key} dropdown`); + const glClass = this.mapping[key].gl; + const element = this.mapping[key].element; + const filterIconPadding = 27; + const dropdownOffset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + + if (!this.mapping[key].reference) { + this.mapping[key].reference = new gl[glClass](this.droplab, element, this.filteredSearchInput); + } + + if (firstLoad) { + this.mapping[key].reference.configure(); + } + + this.mapping[key].reference.setOffset(dropdownOffset); + this.mapping[key].reference.render(firstLoad); + + this.currentDropdown = key; + } + + loadDropdown(dropdownName = '') { let firstLoad = false; - const filteredSearch = document.querySelector('.filtered-search'); if(!this.droplab) { firstLoad = true; this.droplab = new DropLab(); } - dropdownName = dropdownName.toLowerCase(); - - const filterIconPadding = 27; - const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName)[0]; - if (!this.font) { - this.font = window.getComputedStyle(filteredSearch).font; + this.font = window.getComputedStyle(this.filteredSearchInput).font; } - if (match && this.currentDropdown !== match.key) { - console.log(`🦄 load ${match.key} dropdown`); + const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; - const dynamicDropdownPadding = 12; - const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding + dynamicDropdownPadding; - const dropdownAuthorElement = document.querySelector('#js-dropdown-author'); - const dropdownAssigneeElement = document.querySelector('#js-dropdown-assignee'); - const dropdownMilestoneElement = document.querySelector('#js-dropdown-milestone'); - const dropdownLabelElemenet = document.querySelector('#js-dropdown-label'); - - this.dismissCurrentDropdown(); - this.currentDropdown = match.key; - - if (match.key === 'author') { - if (!dropdownAuthor) { - dropdownAuthor = new gl.DropdownAuthor(this.droplab, dropdownAuthorElement, filteredSearch); - } - - dropdownAuthor.setOffset(dropdownOffset); - dropdownAuthor.render(); - } else if (match.key === 'assignee') { - if (!dropdownAssignee) { - dropdownAssignee = new gl.DropdownAssignee(this.droplab, dropdownAssigneeElement, filteredSearch); - } - - dropdownAssignee.setOffset(dropdownOffset); - dropdownAssignee.render(); - } else if (match.key === 'milestone') { - if (!dropdownMilestone) { - dropdownMilestone = new gl.DropdownMilestone(this.droplab, dropdownMilestoneElement, filteredSearch); - } - - dropdownMilestone.setOffset(dropdownOffset); - dropdownMilestone.render(); - } else if (match.key === 'label') { - if (!dropdownLabel) { - dropdownLabel = new gl.DropdownLabel(this.droplab, dropdownLabelElemenet, filteredSearch); - } - - dropdownLabel.setOffset(dropdownOffset); - dropdownLabel.render(); - } - - } else if (!match && this.currentDropdown !== 'hint') { - console.log('🦄 load hint dropdown'); - - const dropdownOffset = gl.text.getTextWidth(filteredSearch.value, this.font) + filterIconPadding; - const dropdownHintElement = document.querySelector('#js-dropdown-hint'); - - this.dismissCurrentDropdown(); - this.currentDropdown = 'hint'; - if (!dropdownHint) { - dropdownHint = new gl.DropdownHint(this.droplab, dropdownHintElement, filteredSearch); - } - - if (firstLoad) { - dropdownHint.configure(); - } - - dropdownHint.setOffset(dropdownOffset); - dropdownHint.render(firstLoad); + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; + this.load(key, firstLoad); } - } - dismissCurrentDropdown() { - // if (this.currentDropdown === 'hint') { - // dropdownHint.hide(); - // } else if (this.currentDropdown === 'author') { - // // dropdownAuthor.hide(); - // } + gl.droplab = this.droplab; } setDropdown() { - const { lastToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); + const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); if (typeof lastToken === 'string') { // Token is not fully initialized yet @@ -228,32 +208,20 @@ } bindEvents() { - const filteredSearchInput = document.querySelector('.filtered-search'); - - filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); - filteredSearchInput.addEventListener('input', toggleClearSearchButton); - filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - document.querySelector('.clear-search').addEventListener('click', this.clearSearch.bind(this)); + this.filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); + this.filteredSearchInput.addEventListener('input', toggleClearSearchButton); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); + this.clearSearchButton.addEventListener('click', this.clearSearch.bind(this)); } clearSearch(e) { e.stopPropagation(); e.preventDefault(); - document.querySelector('.filtered-search').value = ''; - document.querySelector('.clear-search').classList.add('hidden'); + this.filteredSearchInput.value = ''; + this.clearSearchButton.classList.add('hidden'); dropdownHint.resetFilters(); - this.loadDropdown('hint', true); - } - - checkDropdownToken(e) { - const input = e.target.value; - const { lastToken } = this.tokenizer.processTokens(input); - - // Check for dropdown token - if (lastToken[lastToken.length - 1] === ':') { - const token = lastToken.slice(0, -1); - } + this.loadDropdown('hint'); } checkForEnter(e) { diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 0a5de59cb63..53983ef8d6d 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -18,7 +18,7 @@ = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown %ul - %li.filter-dropdown-item{ 'data-value': 'none' } + %li.filter-dropdown-item{ 'data-value': '' } %button.btn.btn-link = icon('search') %span From c18285cec0dadc0af8fa062a10d6cd581efb0b66 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 12:11:29 -0600 Subject: [PATCH 082/185] Fixed issue where dropdown would not open after clicking on a dropdown item --- app/assets/javascripts/droplab/droplab.js | 3 +-- .../shared/issuable/_search_bar.html.haml | 22 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index b17f156acb4..6b326338050 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -234,8 +234,7 @@ require('./window')(function(w){ var self = this; this.windowClickedWrapper = function(e){ var thisTag = e.target; - if(thisTag.tagName === 'LI' || thisTag.tagName === 'A' - || thisTag.tagName === 'BUTTON'){ + if(thisTag.tagName !== 'UL'){ // climb up the tree to find the UL thisTag = utils.closest(thisTag, 'UL'); } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 53983ef8d6d..2d2ecf030a8 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -17,13 +17,13 @@ %button.clear-search.hidden{ type: 'button' } = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown - %ul + %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': '' } %button.btn.btn-link = icon('search') %span Keep typing and press Enter - %ul.filter-dropdown{ 'data-dynamic' => true } + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link %i.fa{ 'class': '{{icon}}'} @@ -31,8 +31,8 @@ {{hint}} %span.js-filter-tag.dropdown-light-content {{tag}} - #js-dropdown-author.dropdown-menu{ 'data-dropdown' => true } - %ul.filter-dropdown{ 'data-dynamic' => true } + #js-dropdown-author.dropdown-menu + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } @@ -41,13 +41,13 @@ {{name}} %span.dropdown-light-content @{{username}} - #js-dropdown-assignee.dropdown-menu{ 'data-dropdown' => true } - %ul + #js-dropdown-assignee.dropdown-menu + %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No assignee %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true } + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } @@ -57,22 +57,22 @@ %span.dropdown-light-content @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } - %ul + %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No milestone %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true } + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } - %ul + %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link No label %li.divider - %ul.filter-dropdown{ 'data-dynamic' => true } + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} From f0608878ce8fd8dfaa91a285eff6d3db11f509c3 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:15:09 -0600 Subject: [PATCH 083/185] Fix bug where dropdowns would not dismiss properly --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 2 +- .../javascripts/filtered_search/dropdown_hint.js.es6 | 9 ++++++--- .../javascripts/filtered_search/dropdown_label.js.es6 | 3 ++- .../filtered_search/dropdown_milestone.js.es6 | 2 +- .../filtered_search/filtered_search_dropdown.js.es6 | 7 +++++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index b2b03b637e7..850cca670e4 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -29,7 +29,7 @@ gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); } - this.dismissDropdown(); + this.dismissDropdown(!dataValueSet); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index f885267880a..ea384af09a9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -34,16 +34,19 @@ itemClicked(e) { const selected = e.detail.selected; - if (!selected.hasAttribute('data-value')) { + + if (selected.hasAttribute('data-value')) { + this.dismissDropdown(); + } else { const token = selected.querySelector('.js-filter-hint').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); } + this.dismissDropdown(); + this.dispatchInputEvent(); } - - this.dismissDropdown(); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index 24a795808ca..c79df0aee4a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -26,7 +26,8 @@ gl.FilteredSearchManager.addWordToInput(labelName); } - this.dismissDropdown(); + // debugger + this.dismissDropdown(!dataValueSet); } renderContent() { diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 458a9b1c5c1..10535097747 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -26,7 +26,7 @@ gl.FilteredSearchManager.addWordToInput(this.getSelectedText(milestoneName)); } - this.dismissDropdown(); + this.dismissDropdown(!dataValueSet); } renderContent() { 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 2f92c7b2e2a..cd46e430e01 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -77,11 +77,15 @@ } dismissDropdown() { + this.getCurrentHook().list.hide(); this.input.focus(); + } + + dispatchInputEvent() { // Propogate input change to FilteredSearchManager // so that it can determine which dropdowns to open this.input.dispatchEvent(new Event('input')); - } + } render(forceRenderContent) { this.setAsDropdown(); @@ -91,7 +95,6 @@ if (firstTimeInitialized || forceRenderContent) { this.renderContent(); } else if(this.getCurrentHook().list.list.id !== this.listId) { - // this.droplab.changeHookList(this.hookId, `#${this.listId}`); this.renderContent(); } } From ddc42a61f064757d69d4c61f784dacbcd1334d4d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:15:38 -0600 Subject: [PATCH 084/185] Fix bug where token values with 2 double quotes were not treated as a complete value --- .../filtered_search/filtered_search_tokenizer.es6 | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index d6df83a3fb9..5ad433f4a09 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -119,19 +119,26 @@ const keyMatch = validTokenKeys.filter(v => v.key === tokenKey)[0]; const symbolMatch = validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; + const doubleQuoteOccurrences = tokenValue.split('"').length - 1; + const singleQuoteOccurrences = tokenValue.split('\'').length - 1; + const doubleQuoteIndex = tokenValue.indexOf('"'); const singleQuoteIndex = tokenValue.indexOf('\''); const doubleQuoteExist = doubleQuoteIndex !== -1; const singleQuoteExist = singleQuoteIndex !== -1; - if ((doubleQuoteExist && !singleQuoteExist) || - (doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex)) { + const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist; + const doubleQuoteIsBeforeSingleQuote = doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; + + const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist; + const singleQuoteIsBeforeDoubleQuote = doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; + + if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) && doubleQuoteOccurrences % 2 !== 0) { // " is found and is in front of ' (if any) lastQuotation = '"'; incompleteToken = true; - } else if ((singleQuoteExist && !doubleQuoteExist) || - (doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex)) { + } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) && singleQuoteOccurrences % 2 !== 0) { // ' is found and is in front of " (if any) lastQuotation = '\''; incompleteToken = true; From 002d17f1b7e4d298f45cb7d0dc944e20cbd734b5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 13:17:19 -0600 Subject: [PATCH 085/185] Fix clear button --- app/assets/javascripts/droplab/droplab.js | 53 ++++++++++--------- .../filtered_search/dropdown_assignee.js.es6 | 4 +- .../filtered_search/dropdown_author.js.es6 | 4 +- .../filtered_search/dropdown_label.js.es6 | 3 +- .../filtered_search/dropdown_milestone.js.es6 | 3 +- .../filtered_search_dropdown.js.es6 | 35 ++++-------- .../filtered_search_manager.js.es6 | 17 ++++-- .../filtered_search_tokenizer.es6 | 10 ++-- 8 files changed, 67 insertions(+), 62 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 6b326338050..42ddb7a4a56 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -51,26 +51,28 @@ Object.assign(DropDown.prototype, { return this.items; }, - addEvents: function() { - var self = this; - // event delegation. - this.list.addEventListener('click', function(e) { - // climb up the tree to find the LI - var selected = utils.closest(e.target, 'LI'); + clickEvent: function(e) { + // climb up the tree to find the LI + var selected = utils.closest(e.target, 'LI'); - if(selected) { - e.preventDefault(); - self.hide(); - var listEvent = new CustomEvent('click.dl', { - detail: { - list: self, - selected: selected, - data: e.target.dataset, - }, - }); - self.list.dispatchEvent(listEvent); - } - }); + if(selected) { + e.preventDefault(); + this.hide(); + var listEvent = new CustomEvent('click.dl', { + detail: { + list: this, + selected: selected, + data: e.target.dataset, + }, + }); + this.list.dispatchEvent(listEvent); + } + }, + + addEvents: function() { + this.clickWrapper = this.clickEvent.bind(this); + // event delegation. + this.list.addEventListener('click', this.clickWrapper); }, toggle: function() { @@ -93,6 +95,7 @@ Object.assign(DropDown.prototype, { // call render manually on data; render: function(data){ + // debugger // empty the list first var sampleItem; var newChildren = []; @@ -134,17 +137,23 @@ Object.assign(DropDown.prototype, { }, show: function() { + // debugger this.list.style.display = 'block'; this.hidden = false; }, hide: function() { + // debugger this.list.style.display = 'none'; this.hidden = true; }, destroy: function() { - this.hide(); + if (!this.hidden) { + this.hide(); + } + + this.list.removeEventListener('click', this.clickWrapper); } }); @@ -257,10 +266,6 @@ require('./window')(function(w){ // list = document.querySelector(list); this.hooks.every(function(hook, i) { if(hook.trigger === trigger) { - // Restore initial State - hook.list.list.innerHTML = hook.list.initialState; - hook.list.hide(); - hook.destroy(); this.hooks.splice(i, 1); this.addHook(trigger, list, plugins, config); diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index 850cca670e4..3420397edda 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -32,9 +32,9 @@ this.dismissDropdown(!dataValueSet); } - renderContent() { - // TODO: Pass elements instead of querySelectors + renderContent(forceShowList = false) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); } getSearchInput() { diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 9bd49ab1a78..f1401f6f9d2 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -28,9 +28,9 @@ this.dismissDropdown(); } - renderContent() { - // TODO: Pass elements instead of querySelectors + renderContent(forceShowList) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); } getSearchInput() { diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index c79df0aee4a..4c9926c1f78 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -30,8 +30,9 @@ this.dismissDropdown(!dataValueSet); } - renderContent() { + renderContent(forceShowList) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); } configure() { diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 10535097747..33967ddff24 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -29,8 +29,9 @@ this.dismissDropdown(!dataValueSet); } - renderContent() { + renderContent(forceShowList = false) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); } configure() { 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 cd46e430e01..163dac65842 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -4,6 +4,7 @@ class FilteredSearchDropdown { constructor(droplab, dropdown, input) { + console.log('constructor'); this.droplab = droplab; this.hookId = 'filtered-search'; this.input = input; @@ -54,8 +55,10 @@ // Overridden by dropdown sub class } - renderContent() { - // Overriden by dropdown sub class + renderContent(forceShowList = false) { + if (forceShowList && this.getCurrentHook().list.hidden) { + this.getCurrentHook().list.show(); + } } setAsDropdown() { @@ -77,7 +80,6 @@ } dismissDropdown() { - this.getCurrentHook().list.hide(); this.input.focus(); } @@ -87,31 +89,16 @@ this.input.dispatchEvent(new Event('input')); } - render(forceRenderContent) { + render(forceRenderContent = false, forceShowList = false) { this.setAsDropdown(); - const firstTimeInitialized = this.getCurrentHook() === undefined; + const currentHook = this.getCurrentHook(); + const firstTimeInitialized = currentHook === undefined; if (firstTimeInitialized || forceRenderContent) { - this.renderContent(); - } else if(this.getCurrentHook().list.list.id !== this.listId) { - this.renderContent(); - } - } - - resetFilters() { - const currentHook = this.getCurrentHook(); - - if (currentHook) { - const list = currentHook.list; - - if (list.data) { - const data = list.data.map((item) => { - item.droplab_hidden = false; - }); - - list.render(data); - } + this.renderContent(forceShowList); + } else if(currentHook.list.list.id !== this.listId) { + this.renderContent(forceShowList); } } 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 9846f3ba50d..4f5d144bff3 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -150,6 +150,7 @@ const element = this.mapping[key].element; const filterIconPadding = 27; const dropdownOffset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + let forceShowList = false; if (!this.mapping[key].reference) { this.mapping[key].reference = new gl[glClass](this.droplab, element, this.filteredSearchInput); @@ -159,8 +160,13 @@ this.mapping[key].reference.configure(); } + if (this.currentDropdown === 'hint') { + // Clicked from hint dropdown + forceShowList = true; + } + this.mapping[key].reference.setOffset(dropdownOffset); - this.mapping[key].reference.render(firstLoad); + this.mapping[key].reference.render(firstLoad, forceShowList); this.currentDropdown = key; } @@ -207,6 +213,12 @@ } } + // dismissCurrentDropdown() { + // if (this.currentDropdown === 'hint') { + // this.mapping['hint'].hide(); + // } + // } + bindEvents() { this.filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); this.filteredSearchInput.addEventListener('input', toggleClearSearchButton); @@ -220,8 +232,7 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); - dropdownHint.resetFilters(); - this.loadDropdown('hint'); + this.setDropdown(); } checkForEnter(e) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index 5ad433f4a09..4abb5e94d81 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -3,11 +3,11 @@ class FilteredSearchTokenizer { // TODO: Remove when going to pro static printTokens(tokens, searchToken, lastToken) { - // console.log('tokens:'); - // tokens.forEach(token => console.log(token)); - // console.log(`search: ${searchToken}`); - // console.log('last token:'); - // console.log(lastToken); + console.log('tokens:'); + tokens.forEach(token => console.log(token)); + console.log(`search: ${searchToken}`); + console.log('last token:'); + console.log(lastToken); } static parseToken(input) { From 21d1d9b2c0d794426de73aab6924dbbe05e61ba5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:10:15 -0600 Subject: [PATCH 086/185] Add username to page_filter_path --- app/helpers/application_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c816b616631..a112928c6de 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -244,7 +244,9 @@ module ApplicationHelper scope: params[:scope], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], + assignee_username: params[:assignee_username], author_id: params[:author_id], + author_username: params[:author_username], search: params[:search], label_name: params[:label_name] } From 0eb9f53715a8fbbe8ede0043aad5a9c9377716e5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:20:41 -0600 Subject: [PATCH 087/185] Prevent droplab from opening dropdown by cleaning it --- .../filtered_search/filtered_search_manager.js.es6 | 4 ++++ 1 file changed, 4 insertions(+) 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 4f5d144bff3..91de7783cc1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -240,6 +240,10 @@ if (e.keyCode === 13) { e.stopPropagation(); e.preventDefault(); + + // Prevent droplab from opening dropdown + this.droplab.destroy(); + this.search(); } } From e5b061b801ee7b806dd401c97a5b268e3f34859a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:36:44 -0600 Subject: [PATCH 088/185] Reposition dropdown when backspace is hit --- .../filtered_search_manager.js.es6 | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 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 91de7783cc1..d21ae70cdb9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -94,6 +94,7 @@ this.setupMapping(); + this.unbindEvents(); document.removeEventListener('page:fetch', this.cleanupWrapper); } @@ -144,12 +145,17 @@ filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; } + updateDropdownOffset(key) { + const filterIconPadding = 27; + const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + + this.mapping[key].reference.setOffset(offset); + } + load(key, firstLoad = false) { console.log(`🦄 load ${key} dropdown`); const glClass = this.mapping[key].gl; const element = this.mapping[key].element; - const filterIconPadding = 27; - const dropdownOffset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; let forceShowList = false; if (!this.mapping[key].reference) { @@ -165,7 +171,7 @@ forceShowList = true; } - this.mapping[key].reference.setOffset(dropdownOffset); + this.updateDropdownOffset(key); this.mapping[key].reference.render(firstLoad, forceShowList); this.currentDropdown = key; @@ -213,17 +219,25 @@ } } - // dismissCurrentDropdown() { - // if (this.currentDropdown === 'hint') { - // this.mapping['hint'].hide(); - // } - // } - bindEvents() { - this.filteredSearchInput.addEventListener('input', this.setDropdown.bind(this)); + this.setDropdownWrapper = this.setDropdown.bind(this); + this.checkForEnterWrapper = this.checkForEnter.bind(this); + this.clearSearchWrapper = this.clearSearch.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + + this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); this.filteredSearchInput.addEventListener('input', toggleClearSearchButton); - this.filteredSearchInput.addEventListener('keydown', this.checkForEnter.bind(this)); - this.clearSearchButton.addEventListener('click', this.clearSearch.bind(this)); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); + this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + } + + unbindEvents() { + this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.removeEventListener('input', toggleClearSearchButton); + this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); + this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); } clearSearch(e) { @@ -235,6 +249,13 @@ this.setDropdown(); } + checkForBackspace(e) { + if (e.keyCode === 8) { + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + } + checkForEnter(e) { // Enter KeyCode if (e.keyCode === 13) { From 88ed015915e63c07181122c461523d3e1610e98e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 15:47:56 -0600 Subject: [PATCH 089/185] Fix clear button so that it resets the dropdowns properly --- .../filtered_search/filtered_search_dropdown.js.es6 | 4 ++++ .../filtered_search/filtered_search_manager.js.es6 | 8 ++++++++ 2 files changed, 12 insertions(+) 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 163dac65842..03835b6522b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -117,6 +117,10 @@ item.droplab_hidden = !match && !matchWithoutPrefix; return item; } + + hideDropdown() { + this.getCurrentHook().list.hide(); + } } global.FilteredSearchDropdown = FilteredSearchDropdown; 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 d21ae70cdb9..0654d7d816a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -246,7 +246,15 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); + + // Force dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown this.setDropdown(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); } checkForBackspace(e) { From 98a95633f558f4f4762d4e523ef0c495d6f79f68 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 9 Dec 2016 16:02:43 -0600 Subject: [PATCH 090/185] Reset filters when clear search is clicked --- .../filtered_search/filtered_search_dropdown.js.es6 | 9 +++++++++ .../filtered_search/filtered_search_manager.js.es6 | 3 +++ 2 files changed, 12 insertions(+) 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 03835b6522b..5186c15cb67 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -121,6 +121,15 @@ hideDropdown() { this.getCurrentHook().list.hide(); } + + resetFilters() { + const hook = this.getCurrentHook(); + const data = hook.list.data; + const results = data.map(function(o) { + o.droplab_hidden = false; + }); + hook.list.render(results); + } } global.FilteredSearchDropdown = FilteredSearchDropdown; 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 0654d7d816a..c7e01fc710d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -247,6 +247,9 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); + // Reset Filters + this.mapping[this.currentDropdown].reference.resetFilters(); + // Force dropdown to hide this.mapping[this.currentDropdown].reference.hideDropdown(); From 7b202460f7333e1a9997f1ee79eca71975cc676f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 11:13:17 -0600 Subject: [PATCH 091/185] Fix ajax bug --- app/assets/javascripts/droplab/droplab.js | 7 +------ app/assets/javascripts/droplab/droplab_ajax_filter.js | 9 +++++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 42ddb7a4a56..359cd82bbcd 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -31,18 +31,13 @@ if ( typeof CustomEvent === "function" ) { var CustomEvent = require('./custom_event_polyfill'); var utils = require('./utils'); -var DropDown = function(list, trigger) { +var DropDown = function(list) { this.hidden = true; this.list = list; - this.trigger = trigger; this.items = []; this.getItems(); this.addEvents(); this.initialState = list.innerHTML; - - if (this.initialState.indexOf('{{') == -1) { - debugger - } }; Object.assign(DropDown.prototype, { diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index c345fda1075..0d6a7892bdc 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -5,6 +5,7 @@ require('../window')(function(w){ w.droplabAjaxFilter = { init: function(hook) { + this.destroyed = false; this.hook = hook; this.notLoading(); @@ -49,14 +50,16 @@ require('../window')(function(w){ } this.loading = true; + this.hook.list.setData([]); var params = config.params || {}; params[config.searchKey] = searchValue; var self = this; this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { - self.hook.restoreInitialState.call(self.hook); - self.hook.list.setData.call(self.hook.list, data[0]); + if (!self.destroyed) { + self.hook.list.setData.call(self.hook.list, data[0]); + } self.notLoading(); }); }, @@ -92,6 +95,8 @@ require('../window')(function(w){ clearTimeout(this.timeout); } + this.destroyed = true; + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper); this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); } From 99ffd0d44e0c813f9c4216b01fa321a5409ed360 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:35:39 -0600 Subject: [PATCH 092/185] Remove ajax clear setData for smoother ux --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 0d6a7892bdc..7603556d2ef 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -51,8 +51,6 @@ require('../window')(function(w){ this.loading = true; - this.hook.list.setData([]); - var params = config.params || {}; params[config.searchKey] = searchValue; var self = this; From 85f1793590b31171bac3c1e1f5d4420d96302551 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:47:36 -0600 Subject: [PATCH 093/185] Pass project ID through the DOM --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 2 +- app/assets/javascripts/filtered_search/dropdown_author.js.es6 | 2 +- .../filtered_search/filtered_search_dropdown.js.es6 | 4 ++++ app/views/shared/issuable/_search_bar.html.haml | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index 3420397edda..ff3fd3a4e2b 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -13,7 +13,7 @@ params: { per_page: 20, active: true, - project_id: 2, + project_id: this.getProjectId(), current_user: true, }, searchValueFunction: this.getSearchInput, diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index f1401f6f9d2..517cbab8ee7 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -13,7 +13,7 @@ params: { per_page: 20, active: true, - project_id: 2, + project_id: this.getProjectId(), current_user: true, }, searchValueFunction: this.getSearchInput, 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 5186c15cb67..478c4e6bf92 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -21,6 +21,10 @@ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); } + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + getCurrentHook() { return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 2d2ecf030a8..86692e77697 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -12,7 +12,7 @@ class: "check_all_issues left" .issues-other-filters.filtered-search-container .filtered-search-input-container - %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search' } + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id } = icon('filter') %button.clear-search.hidden{ type: 'button' } = icon('times') From 575d4491cac225544081135820a1fa53a72d4709 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 13:55:41 -0600 Subject: [PATCH 094/185] Fixed bug where filters were not being reset after being cleared --- .../filtered_search/filtered_search_manager.js.es6 | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 c7e01fc710d..5d38a23d9fd 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -247,15 +247,16 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); - // Reset Filters - this.mapping[this.currentDropdown].reference.resetFilters(); - // Force dropdown to hide + // Force current dropdown to hide this.mapping[this.currentDropdown].reference.hideDropdown(); // Re-Load dropdown this.setDropdown(); + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + // Reposition dropdown so that it is aligned with cursor this.updateDropdownOffset(this.currentDropdown); } From a7ecbf7c79a4789d10493ad3bdb9b02cfd124334 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 14:04:16 -0600 Subject: [PATCH 095/185] Add missing space for extracting params --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 1 + 1 file changed, 1 insertion(+) 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 5d38a23d9fd..055f229cd45 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -33,6 +33,7 @@ if (validCondition) { inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; + inputValue += ' '; } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + From cf3504ed602aa1b979f13a394192dde50b8cffed Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 14:14:21 -0600 Subject: [PATCH 096/185] Make ajax filter more consistent and only filter when typed --- .../javascripts/droplab/droplab_ajax_filter.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 7603556d2ef..f2720a0371b 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -12,7 +12,7 @@ require('../window')(function(w){ this.debounceTriggerWrapper = this.debounceTrigger.bind(this); this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper); this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper); - this.trigger(); + this.trigger(true); }, notLoading: function notLoading() { @@ -22,6 +22,7 @@ require('../window')(function(w){ debounceTrigger: function debounceTrigger(e) { var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93]; var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1; + var focusEvent = false; if (invalidKeyPressed || this.loading) { return; } @@ -30,10 +31,14 @@ require('../window')(function(w){ clearTimeout(this.timeout); } - this.timeout = setTimeout(this.trigger.bind(this), 200); + if (e.type === 'focus') { + focusEvent = true; + } + + this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200); }, - trigger: function trigger() { + trigger: function trigger(getEntireList = false) { var config = this.hook.config.droplabAjaxFilter; var searchValue = this.trigger.value; @@ -45,6 +50,10 @@ require('../window')(function(w){ searchValue = config.searchValueFunction(); } + if (getEntireList) { + searchValue = ''; + } + if (searchValue === config.searchKey) { return this.list.show(); } From efb668208ae25393cd5535ea769c537c55a54313 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:37:26 -0600 Subject: [PATCH 097/185] Add loading message for droplab_ajax --- app/assets/javascripts/droplab/droplab_ajax.js | 15 +++++++++++++++ .../filtered_search/dropdown_label.js.es6 | 4 ++++ .../filtered_search/dropdown_milestone.js.es6 | 4 ++++ app/assets/stylesheets/framework/filters.scss | 4 ++++ 4 files changed, 27 insertions(+) diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index b81663c281d..629260006f3 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -23,6 +23,7 @@ require('../window')(function(w){ }, init: function init(hook) { + var self = this; var config = hook.config.droplabAjax; if (!config || !config.endpoint || !config.method) { @@ -33,7 +34,21 @@ require('../window')(function(w){ return; } + if (config.loadingTemplate) { + var dynamicList = hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', true); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + this._loadUrlData(config.endpoint).then(function(d) { + if (config.loadingTemplate) { + hook.list.list.querySelector('[data-loading-template]').outerHTML = self.listTemplate; + } hook.list[config.method].call(hook.list, d); }).catch(function(e) { if(e.message) { diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index 4c9926c1f78..0912336b6cf 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -10,6 +10,10 @@ droplabAjax: { endpoint: 'labels.json', method: 'setData', + loadingTemplate: ` +
+ +
`, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, '~'), diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 33967ddff24..73d67573868 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -10,6 +10,10 @@ droplabAjax: { endpoint: 'milestones.json', method: 'setData', + loadingTemplate: ` +
+ +
`, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, '%'), diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 205cecb4906..b6c137d647a 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -121,3 +121,7 @@ .hint-dropdown { width: 250px; } + +.filter-dropdown-loading { + padding: 8px 16px; +} From 5066366162cc1da1004bc0d3df4a6377d68dbce4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:37:47 -0600 Subject: [PATCH 098/185] Add loading template to droplab_ajax_filter --- .../droplab/droplab_ajax_filter.js | 24 ++++++++++++++++++- .../filtered_search/dropdown_assignee.js.es6 | 4 ++++ .../filtered_search/dropdown_author.js.es6 | 4 ++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index f2720a0371b..8d024c4b6d7 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -50,6 +50,18 @@ require('../window')(function(w){ searchValue = config.searchValueFunction(); } + if (config.loadingTemplate && this.hook.list.data === undefined || + this.hook.list.data.length === 0) { + var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', true); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + if (getEntireList) { searchValue = ''; } @@ -64,8 +76,18 @@ require('../window')(function(w){ params[config.searchKey] = searchValue; var self = this; this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { + if (config.loadingTemplate && self.hook.list.data === undefined || + self.hook.list.data.length === 0) { + self.hook.list.list.querySelector('[data-loading-template]').outerHTML = self.listTemplate; + } + if (!self.destroyed) { - self.hook.list.setData.call(self.hook.list, data[0]); + if (data[0].length === 0) { + self.hook.list.hide(); + } else { + self.hook.list.show(); + self.hook.list.setData.call(self.hook.list, data[0]); + } } self.notLoading(); }); diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index ff3fd3a4e2b..edc717304b2 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -17,6 +17,10 @@ current_user: true, }, searchValueFunction: this.getSearchInput, + loadingTemplate: ` +
+ +
`, } }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 517cbab8ee7..8d95a879c79 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -17,6 +17,10 @@ current_user: true, }, searchValueFunction: this.getSearchInput, + loadingTemplate: ` +
+ +
`, } }; } From 0791116f5f6a6f3622af6d1caa9ba9da1818275f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 15:40:12 -0600 Subject: [PATCH 099/185] Refactor loadingTemplate to abstract class --- .../javascripts/filtered_search/dropdown_assignee.js.es6 | 5 +---- .../javascripts/filtered_search/dropdown_author.js.es6 | 5 +---- app/assets/javascripts/filtered_search/dropdown_label.js.es6 | 5 +---- .../javascripts/filtered_search/dropdown_milestone.js.es6 | 5 +---- .../filtered_search/filtered_search_dropdown.js.es6 | 3 +++ 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 index edc717304b2..0ce4eebedc9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 @@ -17,10 +17,7 @@ current_user: true, }, searchValueFunction: this.getSearchInput, - loadingTemplate: ` -
- -
`, + loadingTemplate: this.loadingTemplate, } }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 index 8d95a879c79..3dc649cc17d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 @@ -17,10 +17,7 @@ current_user: true, }, searchValueFunction: this.getSearchInput, - loadingTemplate: ` -
- -
`, + loadingTemplate: this.loadingTemplate, } }; } diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 index 0912336b6cf..bf009454de5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 @@ -10,10 +10,7 @@ droplabAjax: { endpoint: 'labels.json', method: 'setData', - loadingTemplate: ` -
- -
`, + loadingTemplate: this.loadingTemplate, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, '~'), diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 index 73d67573868..7f5822aed84 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 @@ -10,10 +10,7 @@ droplabAjax: { endpoint: 'milestones.json', method: 'setData', - loadingTemplate: ` -
- -
`, + loadingTemplate: this.loadingTemplate, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, '%'), 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 478c4e6bf92..6b713a7017e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -9,6 +9,9 @@ this.hookId = 'filtered-search'; this.input = input; this.dropdown = dropdown; + this.loadingTemplate = `
+ +
`; this.bindEvents(); } From f67316a7cb19374812bfc61dc98ece3110538e1b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 16:04:58 -0600 Subject: [PATCH 100/185] Hide list if it is dynamic and there are no items to render --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 8d024c4b6d7..bdd9b059bb3 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -82,12 +82,16 @@ require('../window')(function(w){ } if (!self.destroyed) { - if (data[0].length === 0) { + var hookListChildren = self.hook.list.list.children; + var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); + + if (onlyDynamicList && data[0].length === 0) { self.hook.list.hide(); - } else { + } else if (onlyDynamicList && data[0].length !== 0) { self.hook.list.show(); - self.hook.list.setData.call(self.hook.list, data[0]); } + + self.hook.list.setData.call(self.hook.list, data[0]); } self.notLoading(); }); From 7eb888e60e506ab25ba3593cde53dc94c19b9d7e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 16:06:45 -0600 Subject: [PATCH 101/185] Only return data response for droplab ajax filter --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index bdd9b059bb3..943ee9fa0a4 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -85,13 +85,13 @@ require('../window')(function(w){ var hookListChildren = self.hook.list.list.children; var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); - if (onlyDynamicList && data[0].length === 0) { + if (onlyDynamicList && data.length === 0) { self.hook.list.hide(); - } else if (onlyDynamicList && data[0].length !== 0) { + } else if (onlyDynamicList && data.length !== 0) { self.hook.list.show(); } - self.hook.list.setData.call(self.hook.list, data[0]); + self.hook.list.setData.call(self.hook.list, data); } self.notLoading(); }); @@ -105,7 +105,7 @@ require('../window')(function(w){ if(xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { var data = JSON.parse(xhr.responseText); - return resolve([data, xhr]); + return resolve(data); } else { return reject([xhr.responseText, xhr.status]); } From 172ce7001ae606f7f0df56aedc6c5d6ad1d9305c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 18:42:20 -0600 Subject: [PATCH 102/185] Fix casing and add upcoming milestone filter --- app/views/shared/issuable/_search_bar.html.haml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 86692e77697..7ebc4d6b153 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -45,7 +45,7 @@ %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link - No assignee + No Assignee %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item @@ -60,7 +60,10 @@ %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link - No milestone + No Milestone + %li.filter-dropdown-item{ 'data-value': 'upcoming' } + %button.btn.btn-link + Upcoming %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item @@ -70,7 +73,7 @@ %ul{ 'data-dropdown' => true } %li.filter-dropdown-item{ 'data-value': 'none' } %button.btn.btn-link - No label + No Label %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item From acfe967eb587078672953971673e2cd2c02b43ee Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 10 Dec 2016 18:53:56 -0600 Subject: [PATCH 103/185] Add mobile viewport --- app/assets/stylesheets/framework/mobile.scss | 6 +++++- app/views/shared/issuable/_search_bar.html.haml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 7eb9962ba33..db3bf9f86c4 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -23,12 +23,16 @@ margin-right: 0; } - .issues-details-filters, + .issues-details-filters:not(.filtered-search-block), .dash-projects-filters, .check-all-holder { display: none; } + .issues-holder .issue-check { + display: none; + } + .rss-btn { display: none; } diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 7ebc4d6b153..f7c72e3ced8 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -2,7 +2,7 @@ - boards_page = controller.controller_name == 'boards' .issues-filters - .issues-details-filters.row-content-block.second-block + .issues-details-filters.row-content-block.second-block.filtered-search-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do - if params[:search].present? = hidden_field_tag :search, params[:search] From 5c0802deae6a3a87e7a497d3250cbeb98e61045b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:05:33 -0600 Subject: [PATCH 104/185] Add check in case the data attribute does not exist --- app/assets/javascripts/droplab/droplab_ajax.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 629260006f3..ebb518eeef4 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -47,7 +47,11 @@ require('../window')(function(w){ this._loadUrlData(config.endpoint).then(function(d) { if (config.loadingTemplate) { - hook.list.list.querySelector('[data-loading-template]').outerHTML = self.listTemplate; + var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } } hook.list[config.method].call(hook.list, d); }).catch(function(e) { From 46a1f36986aa61597f54d14c76b1d2258b0933e5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:05:55 -0600 Subject: [PATCH 105/185] Refactor dropdown filters --- .../filtered_search/dropdown_author.js.es6 | 57 ------------------- .../filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search/dropdown_label.js.es6 | 45 --------------- ...estone.js.es6 => dropdown_non_user.js.es6} | 18 +++--- ...n_assignee.js.es6 => dropdown_user.js.es6} | 14 ++--- .../filtered_search_manager.js.es6 | 16 ++++-- .../shared/issuable/_search_bar.html.haml | 4 +- 7 files changed, 28 insertions(+), 128 deletions(-) delete mode 100644 app/assets/javascripts/filtered_search/dropdown_author.js.es6 delete mode 100644 app/assets/javascripts/filtered_search/dropdown_label.js.es6 rename app/assets/javascripts/filtered_search/{dropdown_milestone.js.es6 => dropdown_non_user.js.es6} (65%) rename app/assets/javascripts/filtered_search/{dropdown_assignee.js.es6 => dropdown_user.js.es6} (85%) diff --git a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 b/app/assets/javascripts/filtered_search/dropdown_author.js.es6 deleted file mode 100644 index 3dc649cc17d..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_author.js.es6 +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable no-param-reassign */ -/*= require filtered_search/filtered_search_dropdown */ - -((global) => { - class DropdownAuthor extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); - this.listId = 'js-dropdown-author'; - this.config = { - droplabAjaxFilter: { - endpoint: '/autocomplete/users.json', - searchKey: 'search', - params: { - per_page: 20, - active: true, - project_id: this.getProjectId(), - current_user: true, - }, - searchValueFunction: this.getSearchInput, - loadingTemplate: this.loadingTemplate, - } - }; - } - - itemClicked(e) { - const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); - - this.dismissDropdown(); - } - - renderContent(forceShowList) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); - super.renderContent(forceShowList); - } - - getSearchInput() { - const query = document.querySelector('.filtered-search').value; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1); - const hasPrefix = valueWithoutColon[0] === '@'; - const valueWithoutPrefix = valueWithoutColon.slice(1); - - if (hasPrefix) { - return valueWithoutPrefix; - } else { - return valueWithoutColon; - } - } - - configure() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); - } - } - - global.DropdownAuthor = DropdownAuthor; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index ea384af09a9..d445a796f43 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -23,7 +23,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); - this.listId = 'js-dropdown-hint'; + this.listId = dropdown.id; this.config = { droplabFilter: { template: 'hint', diff --git a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 b/app/assets/javascripts/filtered_search/dropdown_label.js.es6 deleted file mode 100644 index bf009454de5..00000000000 --- a/app/assets/javascripts/filtered_search/dropdown_label.js.es6 +++ /dev/null @@ -1,45 +0,0 @@ -/* eslint-disable no-param-reassign */ -/*= require filtered_search/filtered_search_dropdown */ - -((global) => { - class DropdownLabel extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); - this.listId = 'js-dropdown-label'; - this.config = { - droplabAjax: { - endpoint: 'labels.json', - method: 'setData', - loadingTemplate: this.loadingTemplate, - }, - droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, '~'), - } - }; - } - - itemClicked(e) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); - - if (!dataValueSet) { - const labelTitle = e.detail.selected.querySelector('.label-title').innerText.trim(); - const labelName = `~${this.getEscapedText(labelTitle)}`; - gl.FilteredSearchManager.addWordToInput(labelName); - } - - // debugger - this.dismissDropdown(!dataValueSet); - } - - renderContent(forceShowList) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); - super.renderContent(forceShowList); - } - - configure() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); - } - } - - global.DropdownLabel = DropdownLabel; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 similarity index 65% rename from app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 rename to app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 7f5822aed84..05c9284bc96 100644 --- a/app/assets/javascripts/filtered_search/dropdown_milestone.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -2,18 +2,18 @@ /*= require filtered_search/filtered_search_dropdown */ ((global) => { - class DropdownMilestone extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { + class DropdownNonUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, endpoint, symbol) { super(droplab, dropdown, input); - this.listId = 'js-dropdown-milestone'; + this.listId = dropdown.id; this.config = { droplabAjax: { - endpoint: 'milestones.json', + endpoint: endpoint, method: 'setData', loadingTemplate: this.loadingTemplate, }, droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, '%'), + filterFunction: this.filterWithSymbol.bind(this, symbol), } }; } @@ -22,9 +22,9 @@ const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - const milestoneTitle = e.detail.selected.querySelector('.btn-link').innerText.trim(); - const milestoneName = `%${this.getEscapedText(milestoneTitle)}`; - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(milestoneName)); + const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); + const name = `%${this.getEscapedText(title)}`; + gl.FilteredSearchManager.addWordToInput(this.getSelectedText(name)); } this.dismissDropdown(!dataValueSet); @@ -40,5 +40,5 @@ } } - global.DropdownMilestone = DropdownMilestone; + global.DropdownNonUser = DropdownNonUser; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 similarity index 85% rename from app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 rename to app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 0ce4eebedc9..1a597bbbc9d 100644 --- a/app/assets/javascripts/filtered_search/dropdown_assignee.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -2,10 +2,10 @@ /*= require filtered_search/filtered_search_dropdown */ ((global) => { - class DropdownAssignee extends gl.FilteredSearchDropdown { + class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); - this.listId = 'js-dropdown-assignee'; + this.listId = dropdown.id; this.config = { droplabAjaxFilter: { endpoint: '/autocomplete/users.json', @@ -18,7 +18,7 @@ }, searchValueFunction: this.getSearchInput, loadingTemplate: this.loadingTemplate, - } + }, }; } @@ -45,11 +45,7 @@ const hasPrefix = valueWithoutColon[0] === '@'; const valueWithoutPrefix = valueWithoutColon.slice(1); - if (hasPrefix) { - return valueWithoutPrefix; - } else { - return valueWithoutColon; - } + return hasPrefix ? valueWithoutPrefix : valueWithoutColon; } configure() { @@ -57,5 +53,5 @@ } } - global.DropdownAssignee = DropdownAssignee; + global.DropdownUser = DropdownUser; })(window.gl || (window.gl = {})); 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 055f229cd45..c92d669114e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -103,22 +103,24 @@ this.mapping = { author: { reference: null, - gl: 'DropdownAuthor', + gl: 'DropdownUser', element: document.querySelector('#js-dropdown-author'), }, assignee: { reference: null, - gl: 'DropdownAssignee', + gl: 'DropdownUser', element: document.querySelector('#js-dropdown-assignee'), }, milestone: { reference: null, - gl: 'DropdownMilestone', + gl: 'DropdownNonUser', + extraArguments: ['milestones.json', '%'], element: document.querySelector('#js-dropdown-milestone'), }, label: { reference: null, - gl: 'DropdownLabel', + gl: 'DropdownNonUser', + extraArguments: ['labels.json', '~'], element: document.querySelector('#js-dropdown-label'), }, hint: { @@ -160,7 +162,11 @@ let forceShowList = false; if (!this.mapping[key].reference) { - this.mapping[key].reference = new gl[glClass](this.droplab, element, this.filteredSearchInput); + var dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput]; + const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); + + this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); } if (firstLoad) { diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index f7c72e3ced8..335552c0a26 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -67,7 +67,7 @@ %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item - %button.btn.btn-link + %button.btn.btn-link.js-data-value {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true } @@ -79,7 +79,7 @@ %li.filter-dropdown-item %button.btn.btn-link %span.dropdown-label-box{ 'style': 'background: {{color}}'} - %span.label-title + %span.label-title.js-data-value {{title}} .pull-right - if boards_page From f4db75728e8c16876cb3f74e12d4d707ab8f47c1 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 11 Dec 2016 18:11:58 -0600 Subject: [PATCH 106/185] Refactor filtered_search_dropdown --- .../filtered_search/dropdown_non_user.js.es6 | 39 ++++++++++++++++++- .../filtered_search/dropdown_user.js.es6 | 4 ++ .../filtered_search_dropdown.js.es6 | 39 ------------------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 05c9284bc96..f03c27c3ec0 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -6,6 +6,7 @@ constructor(droplab, dropdown, input, endpoint, symbol) { super(droplab, dropdown, input); this.listId = dropdown.id; + this.symbol = symbol; this.config = { droplabAjax: { endpoint: endpoint, @@ -13,7 +14,7 @@ loadingTemplate: this.loadingTemplate, }, droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, symbol), + filterFunction: this.filterWithSymbol.bind(this, this.symbol), } }; } @@ -23,13 +24,47 @@ if (!dataValueSet) { const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); - const name = `%${this.getEscapedText(title)}`; + const name = `${this.symbol}${this.getEscapedText(title)}`; gl.FilteredSearchManager.addWordToInput(this.getSelectedText(name)); } this.dismissDropdown(!dataValueSet); } + getEscapedText(text) { + let escapedText = text; + + // Encapsulate value with quotes if it has spaces + if (text.indexOf(' ') !== -1) { + if (text.indexOf('"') !== -1) { + // Use single quotes if value contains double quotes + escapedText = `'${text}'`; + } else { + // Known side effect: values's with both single and double quotes + // won't escape properly + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + filterWithSymbol(filterSymbol, item, query) { + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1).toLowerCase(); + const prefix = valueWithoutColon[0]; + const valueWithoutPrefix = valueWithoutColon.slice(1); + + const title = item.title.toLowerCase(); + + // Eg. filterSymbol = ~ for labels + const matchWithoutPrefix = prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + const match = title.indexOf(valueWithoutColon) !== -1; + + item.droplab_hidden = !match && !matchWithoutPrefix; + return item; + } + renderContent(forceShowList = false) { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); super.renderContent(forceShowList); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 1a597bbbc9d..6827ab1658a 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -38,6 +38,10 @@ super.renderContent(forceShowList); } + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + getSearchInput() { const query = document.querySelector('.filtered-search').value; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); 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 6b713a7017e..c63ba1acf0b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -4,7 +4,6 @@ class FilteredSearchDropdown { constructor(droplab, dropdown, input) { - console.log('constructor'); this.droplab = droplab; this.hookId = 'filtered-search'; this.input = input; @@ -24,32 +23,10 @@ this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); } - getProjectId() { - return this.input.getAttribute('data-project-id'); - } - getCurrentHook() { return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } - getEscapedText(text) { - let escapedText = text; - - // Encapsulate value with quotes if it has spaces - if (text.indexOf(' ') !== -1) { - if (text.indexOf('"') !== -1) { - // Use single quotes if value contains double quotes - escapedText = `'${text}'`; - } else { - // Known side effect: values's with both single and double quotes - // won't escape properly - escapedText = `"${text}"`; - } - } - - return escapedText; - } - getSelectedText(selectedToken) { // TODO: Get last word from FilteredSearchTokenizer const lastWord = this.input.value.split(' ').last(); @@ -109,22 +86,6 @@ } } - filterWithSymbol(filterSymbol, item, query) { - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const prefix = valueWithoutColon[0]; - const valueWithoutPrefix = valueWithoutColon.slice(1); - - const title = item.title.toLowerCase(); - - // Eg. filterSymbol = ~ for labels - const matchWithoutPrefix = prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; - const match = title.indexOf(valueWithoutColon) !== -1; - - item.droplab_hidden = !match && !matchWithoutPrefix; - return item; - } - hideDropdown() { this.getCurrentHook().list.hide(); } From 513cdda31667f0058b24e8f66d87ddfcf89b7fb4 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:21:38 -0600 Subject: [PATCH 107/185] Refactor filtered search manager --- .../filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search/dropdown_non_user.js.es6 | 2 +- .../filtered_search/dropdown_user.js.es6 | 2 +- .../filtered_search_dropdown.js.es6 | 2 +- .../filtered_search_dropdown_manager.js.es6 | 174 ++++++++++++++++++ .../filtered_search_manager.js.es6 | 160 +--------------- 6 files changed, 185 insertions(+), 157 deletions(-) create mode 100644 app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index d445a796f43..53952e6bc63 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -42,7 +42,7 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(token)); + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(token)); } this.dismissDropdown(); this.dispatchInputEvent(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index f03c27c3ec0..e4df39cfde1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -25,7 +25,7 @@ if (!dataValueSet) { const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); const name = `${this.symbol}${this.getEscapedText(title)}`; - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(name)); + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(name)); } this.dismissDropdown(!dataValueSet); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 6827ab1658a..d3c3be9b914 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -27,7 +27,7 @@ if (!dataValueSet) { const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchManager.addWordToInput(this.getSelectedText(username)); + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(username)); } this.dismissDropdown(!dataValueSet); 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 c63ba1acf0b..38ecbbf552d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -57,7 +57,7 @@ const dataValue = selected.getAttribute('data-value'); if (dataValue) { - gl.FilteredSearchManager.addWordToInput(dataValue); + gl.FilteredSearchDropdownManager.addWordToInput(dataValue); } return dataValue !== null; 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 new file mode 100644 index 00000000000..67a474985c0 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -0,0 +1,174 @@ +/* eslint-disable no-param-reassign */ +((global) => { + class FilteredSearchDropdownManager { + constructor() { + this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchInput = document.querySelector('.filtered-search'); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } + + cleanup() { + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + this.setupMapping(); + + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['milestones.json', '%'], + element: document.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['labels.json', '~'], + element: document.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: document.querySelector('#js-dropdown-hint'), + }, + } + } + + static addWordToInput(word, addSpace) { + const filteredSearchInput = document.querySelector('.filtered-search') + const filteredSearchValue = filteredSearchInput.value; + const hasExistingValue = filteredSearchValue.length !== 0; + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); + + if (lastToken.hasOwnProperty('key')) { + console.log(lastToken); + // Spaces inside the token means that the token value will be escaped by quotes + const hasQuotes = lastToken.value.indexOf(' ') !== -1; + const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; + filteredSearchInput.value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); + } + + filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; + } + + updateCurrentDropdownOffset() { + this.updateDropdownOffset(this.currentDropdown); + } + + updateDropdownOffset(key) { + const filterIconPadding = 27; + const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + + this.mapping[key].reference.setOffset(offset); + } + + load(key, firstLoad = false) { + console.log(`🦄 load ${key} dropdown`); + const glClass = this.mapping[key].gl; + const element = this.mapping[key].element; + let forceShowList = false; + + if (!this.mapping[key].reference) { + var dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput]; + const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); + + this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); + } + + if (firstLoad) { + this.mapping[key].reference.configure(); + } + + if (this.currentDropdown === 'hint') { + // Clicked from hint dropdown + forceShowList = true; + } + + this.updateDropdownOffset(key); + this.mapping[key].reference.render(firstLoad, forceShowList); + + this.currentDropdown = key; + } + + loadDropdown(dropdownName = '') { + let firstLoad = false; + + if(!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } + + const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; + + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; + this.load(key, firstLoad); + } + + gl.droplab = this.droplab; + } + + setDropdown() { + const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + + if (typeof lastToken === 'string') { + // Token is not fully initialized yet + // because it has no value + // Eg. token = 'label:' + const { tokenKey } = this.tokenizer.parseToken(lastToken); + this.loadDropdown(tokenKey); + } else if (lastToken.hasOwnProperty('key')) { + // Token has been initialized into an object + // because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); + } + } + + resetDropdowns() { + // Force current dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown + this.setDropdown(); + + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + + destroyDroplab() { + this.droplab.destroy(); + } + } + + global.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(window.gl || (window.gl = {})); 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 c92d669114e..d9ea44b3a13 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -75,159 +75,24 @@ this.tokenizer = gl.FilteredSearchTokenizer; this.filteredSearchInput = document.querySelector('.filtered-search'); this.clearSearchButton = document.querySelector('.clear-search'); + this.dropdownManager = new gl.FilteredSearchDropdownManager(); - this.setupMapping(); + this.dropdownManager.setupMapping(); this.bindEvents(); loadSearchParamsFromURL(); - this.setDropdown(); + this.dropdownManager.setDropdown(); this.cleanupWrapper = this.cleanup.bind(this); document.addEventListener('page:fetch', this.cleanupWrapper); } cleanup() { - console.log('cleanup') - - if (this.droplab) { - this.droplab.destroy(); - this.droplab = null; - } - - this.setupMapping(); - this.unbindEvents(); document.removeEventListener('page:fetch', this.cleanupWrapper); } - setupMapping() { - this.mapping = { - author: { - reference: null, - gl: 'DropdownUser', - element: document.querySelector('#js-dropdown-author'), - }, - assignee: { - reference: null, - gl: 'DropdownUser', - element: document.querySelector('#js-dropdown-assignee'), - }, - milestone: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: ['milestones.json', '%'], - element: document.querySelector('#js-dropdown-milestone'), - }, - label: { - reference: null, - gl: 'DropdownNonUser', - extraArguments: ['labels.json', '~'], - element: document.querySelector('#js-dropdown-label'), - }, - hint: { - reference: null, - gl: 'DropdownHint', - element: document.querySelector('#js-dropdown-hint'), - }, - } - } - - static addWordToInput(word, addSpace) { - const filteredSearchInput = document.querySelector('.filtered-search') - const filteredSearchValue = filteredSearchInput.value; - const hasExistingValue = filteredSearchValue.length !== 0; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); - - if (lastToken.hasOwnProperty('key')) { - console.log(lastToken); - // Spaces inside the token means that the token value will be escaped by quotes - const hasQuotes = lastToken.value.indexOf(' ') !== -1; - const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; - filteredSearchInput.value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); - } - - filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; - } - - updateDropdownOffset(key) { - const filterIconPadding = 27; - const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; - - this.mapping[key].reference.setOffset(offset); - } - - load(key, firstLoad = false) { - console.log(`🦄 load ${key} dropdown`); - const glClass = this.mapping[key].gl; - const element = this.mapping[key].element; - let forceShowList = false; - - if (!this.mapping[key].reference) { - var dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput]; - const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); - - this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); - } - - if (firstLoad) { - this.mapping[key].reference.configure(); - } - - if (this.currentDropdown === 'hint') { - // Clicked from hint dropdown - forceShowList = true; - } - - this.updateDropdownOffset(key); - this.mapping[key].reference.render(firstLoad, forceShowList); - - this.currentDropdown = key; - } - - loadDropdown(dropdownName = '') { - let firstLoad = false; - - if(!this.droplab) { - firstLoad = true; - this.droplab = new DropLab(); - } - - if (!this.font) { - this.font = window.getComputedStyle(this.filteredSearchInput).font; - } - - const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); - const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; - - if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { - const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; - this.load(key, firstLoad); - } - - gl.droplab = this.droplab; - } - - setDropdown() { - const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); - - if (typeof lastToken === 'string') { - // Token is not fully initialized yet - // because it has no value - // Eg. token = 'label:' - const { tokenKey } = this.tokenizer.parseToken(lastToken); - this.loadDropdown(tokenKey); - } else if (lastToken.hasOwnProperty('key')) { - // Token has been initialized into an object - // because it has a value - this.loadDropdown(lastToken.key); - } else { - this.loadDropdown('hint'); - } - } - bindEvents() { - this.setDropdownWrapper = this.setDropdown.bind(this); + this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); this.checkForEnterWrapper = this.checkForEnter.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); @@ -254,24 +119,13 @@ this.filteredSearchInput.value = ''; this.clearSearchButton.classList.add('hidden'); - - // Force current dropdown to hide - this.mapping[this.currentDropdown].reference.hideDropdown(); - - // Re-Load dropdown - this.setDropdown(); - - // Reset filters for current dropdown - this.mapping[this.currentDropdown].reference.resetFilters(); - - // Reposition dropdown so that it is aligned with cursor - this.updateDropdownOffset(this.currentDropdown); + this.dropdownManager.resetDropdowns(); } checkForBackspace(e) { if (e.keyCode === 8) { // Reposition dropdown so that it is aligned with cursor - this.updateDropdownOffset(this.currentDropdown); + this.dropdownManager.updateCurrentDropdownOffset(); } } @@ -282,7 +136,7 @@ e.preventDefault(); // Prevent droplab from opening dropdown - this.droplab.destroy(); + this.dropdownManager.destroyDroplab(); this.search(); } From 6c811d478d60246a8c2abf4b1bc4fd252d344ed9 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:47:21 -0600 Subject: [PATCH 108/185] Remove show() as it is automatically called on setData when there is data --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 943ee9fa0a4..6e1eb080e3b 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -87,8 +87,6 @@ require('../window')(function(w){ if (onlyDynamicList && data.length === 0) { self.hook.list.hide(); - } else if (onlyDynamicList && data.length !== 0) { - self.hook.list.show(); } self.hook.list.setData.call(self.hook.list, data); From 2bbc44cb7edb81d1e83836574573b365f0b4d1cb Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 09:55:27 -0600 Subject: [PATCH 109/185] Refactor itemClicked --- .../filtered_search/dropdown_non_user.js.es6 | 11 +++-------- .../javascripts/filtered_search/dropdown_user.js.es6 | 11 +++-------- .../filtered_search/filtered_search_dropdown.js.es6 | 11 +++++++++-- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index e4df39cfde1..752a9a6e242 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -20,15 +20,10 @@ } itemClicked(e) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); - - if (!dataValueSet) { + super.itemClicked(e, (selected) => { const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); - const name = `${this.symbol}${this.getEscapedText(title)}`; - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(name)); - } - - this.dismissDropdown(!dataValueSet); + return `${this.symbol}${this.getEscapedText(title)}`; + }); } getEscapedText(text) { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index d3c3be9b914..749fb9d90aa 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -23,14 +23,9 @@ } itemClicked(e) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); - - if (!dataValueSet) { - const username = e.detail.selected.querySelector('.dropdown-light-content').innerText.trim(); - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(username)); - } - - this.dismissDropdown(!dataValueSet); + super.itemClicked(e, (selected) => { + return selected.querySelector('.dropdown-light-content').innerText.trim(); + }); } renderContent(forceShowList = false) { 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 38ecbbf552d..990d56188cb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -35,8 +35,15 @@ return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); } - itemClicked(e) { - // Overridden by dropdown sub class + itemClicked(e, getValueFunction) { + const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + + if (!dataValueSet) { + const value = getValueFunction(e.detail.selected) + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(value)); + } + + this.dismissDropdown(); } renderContent(forceShowList = false) { From 5589ab1e0be2d682a8be424289d17b4e566caba0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:28:22 -0600 Subject: [PATCH 110/185] Refactor and add comments --- .../filtered_search/dropdown_hint.js.es6 | 3 +- .../filtered_search/dropdown_non_user.js.es6 | 17 ++++---- .../filtered_search/dropdown_user.js.es6 | 3 +- .../filtered_search_dropdown.js.es6 | 39 +++++++++--------- .../filtered_search_dropdown_manager.js.es6 | 41 ++++++++++--------- .../filtered_search_manager.js.es6 | 9 ++-- .../filtered_search_tokenizer.es6 | 12 ------ 7 files changed, 58 insertions(+), 66 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 53952e6bc63..43a0b1da0fe 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -23,7 +23,6 @@ class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); - this.listId = dropdown.id; this.config = { droplabFilter: { template: 'hint', @@ -66,7 +65,7 @@ return item; } - configure() { + init() { this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 752a9a6e242..0969df65836 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -5,7 +5,6 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, endpoint, symbol) { super(droplab, dropdown, input); - this.listId = dropdown.id; this.symbol = symbol; this.config = { droplabAjax: { @@ -28,15 +27,17 @@ getEscapedText(text) { let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + const hasSingleQuote = text.indexOf('\'') !== -1; // Encapsulate value with quotes if it has spaces - if (text.indexOf(' ') !== -1) { - if (text.indexOf('"') !== -1) { - // Use single quotes if value contains double quotes + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { escapedText = `'${text}'`; - } else { - // Known side effect: values's with both single and double quotes - // won't escape properly + } else if (hasSingleQuote) { escapedText = `"${text}"`; } } @@ -65,7 +66,7 @@ super.renderContent(forceShowList); } - configure() { + init() { this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 749fb9d90aa..8bc274e0b12 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -5,7 +5,6 @@ class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); - this.listId = dropdown.id; this.config = { droplabAjaxFilter: { endpoint: '/autocomplete/users.json', @@ -47,7 +46,7 @@ return hasPrefix ? valueWithoutPrefix : valueWithoutColon; } - configure() { + init() { this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); } } 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 990d56188cb..85d684e3058 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -46,14 +46,8 @@ this.dismissDropdown(); } - renderContent(forceShowList = false) { - if (forceShowList && this.getCurrentHook().list.hidden) { - this.getCurrentHook().list.show(); - } - } - setAsDropdown() { - this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.listId}`); + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); } setOffset(offset = 0) { @@ -67,17 +61,14 @@ gl.FilteredSearchDropdownManager.addWordToInput(dataValue); } + // Return boolean based on whether it was set return dataValue !== null; } - dismissDropdown() { - this.input.focus(); - } - - dispatchInputEvent() { - // Propogate input change to FilteredSearchManager - // so that it can determine which dropdowns to open - this.input.dispatchEvent(new Event('input')); + renderContent(forceShowList = false) { + if (forceShowList && this.getCurrentHook().list.hidden) { + this.getCurrentHook().list.show(); + } } render(forceRenderContent = false, forceShowList = false) { @@ -88,11 +79,23 @@ if (firstTimeInitialized || forceRenderContent) { this.renderContent(forceShowList); - } else if(currentHook.list.list.id !== this.listId) { + } else if(currentHook.list.list.id !== this.dropdown.id) { this.renderContent(forceShowList); } } + dismissDropdown() { + // Focusing on the input will dismiss dropdown + // (default droplab functionality) + this.input.focus(); + } + + dispatchInputEvent() { + // Propogate input change to FilteredSearchDropdownManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new Event('input')); + } + hideDropdown() { this.getCurrentHook().list.hide(); } @@ -100,9 +103,7 @@ resetFilters() { const hook = this.getCurrentHook(); const data = hook.list.data; - const results = data.map(function(o) { - o.droplab_hidden = false; - }); + const results = data.map(o => o.droplab_hidden = false); hook.list.render(results); } } 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 67a474985c0..a0764c275e5 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 @@ -5,6 +5,8 @@ this.tokenizer = gl.FilteredSearchTokenizer; this.filteredSearchInput = document.querySelector('.filtered-search'); + this.setupMapping(); + this.cleanupWrapper = this.cleanup.bind(this); document.addEventListener('page:fetch', this.cleanupWrapper); } @@ -52,21 +54,22 @@ } } - static addWordToInput(word, addSpace) { - const filteredSearchInput = document.querySelector('.filtered-search') - const filteredSearchValue = filteredSearchInput.value; - const hasExistingValue = filteredSearchValue.length !== 0; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(filteredSearchValue); + static addWordToInput(word, addSpace = false) { + const input = document.querySelector('.filtered-search') + const value = input.value; + const hasExistingValue = value.length !== 0; + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value); if (lastToken.hasOwnProperty('key')) { - console.log(lastToken); // Spaces inside the token means that the token value will be escaped by quotes const hasQuotes = lastToken.value.indexOf(' ') !== -1; + + // Add 2 length to account for the length of the front and back quotes const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; - filteredSearchInput.value = filteredSearchValue.slice(0, -1 * (lengthToRemove)); + input.value = value.slice(0, -1 * (lengthToRemove)); } - filteredSearchInput.value += hasExistingValue && addSpace ? ` ${word}` : word; + input.value += hasExistingValue && addSpace ? ` ${word}` : word; } updateCurrentDropdownOffset() { @@ -74,6 +77,10 @@ } updateDropdownOffset(key) { + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } + const filterIconPadding = 27; const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; @@ -87,19 +94,20 @@ let forceShowList = false; if (!this.mapping[key].reference) { - var dl = this.droplab; + const dl = this.droplab; const defaultArguments = [null, dl, element, this.filteredSearchInput]; const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); + // Passing glArguments to `new gl[glClass]()` this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); } if (firstLoad) { - this.mapping[key].reference.configure(); + this.mapping[key].reference.init(); } if (this.currentDropdown === 'hint') { - // Clicked from hint dropdown + // Force the dropdown to show if it was clicked from the hint dropdown forceShowList = true; } @@ -117,15 +125,12 @@ this.droplab = new DropLab(); } - if (!this.font) { - this.font = window.getComputedStyle(this.filteredSearchInput).font; - } - const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + // `hint` is not listed as a tokenKey (since it is not a real `filter`) const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; this.load(key, firstLoad); } @@ -137,14 +142,12 @@ const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); if (typeof lastToken === 'string') { - // 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:' const { tokenKey } = this.tokenizer.parseToken(lastToken); this.loadDropdown(tokenKey); } else if (lastToken.hasOwnProperty('key')) { - // 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); } else { this.loadDropdown('hint'); 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 d9ea44b3a13..d3bccc4b14c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ ((global) => { + // TODO: Encapsulate inside class? function toggleClearSearchButton(e) { const clearSearchButton = document.querySelector('.clear-search'); @@ -25,6 +26,7 @@ let conditionIndex = 0; const validCondition = gl.FilteredSearchTokenKeys.get() .filter(v => v.conditions && v.conditions.filter((c, index) => { + // TODO: Add comment here if (c.url === p) { conditionIndex = index; } @@ -32,6 +34,7 @@ })[0])[0]; if (validCondition) { + // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; inputValue += ' '; } else { @@ -77,7 +80,6 @@ this.clearSearchButton = document.querySelector('.clear-search'); this.dropdownManager = new gl.FilteredSearchDropdownManager(); - this.dropdownManager.setupMapping(); this.bindEvents(); loadSearchParamsFromURL(); this.dropdownManager.setDropdown(); @@ -130,7 +132,6 @@ } checkForEnter(e) { - // Enter KeyCode if (e.keyCode === 13) { e.stopPropagation(); e.preventDefault(); @@ -143,7 +144,6 @@ } search() { - console.log('search'); let path = '?scope=all&utf8=✓'; // Check current state @@ -152,9 +152,10 @@ const defaultState = 'opened'; let currentState = defaultState; - const { tokens, searchToken } = this.tokenizer.processTokens(document.querySelector('.filtered-search').value); + const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); if (stateIndex !== -1) { + // TODO: Add comment here const remaining = currentPath.slice(stateIndex + 6); const separatorIndex = remaining.indexOf('&'); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index 4abb5e94d81..ac45d3b7986 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -1,15 +1,6 @@ /* eslint-disable no-param-reassign */ ((global) => { class FilteredSearchTokenizer { - // TODO: Remove when going to pro - static printTokens(tokens, searchToken, lastToken) { - console.log('tokens:'); - tokens.forEach(token => console.log(token)); - console.log(`search: ${searchToken}`); - console.log('last token:'); - console.log(lastToken); - } - static parseToken(input) { const colonIndex = input.indexOf(':'); let tokenKey; @@ -163,9 +154,6 @@ searchToken = searchTerms.trim(); - // TODO: Remove when going to PRO - gl.FilteredSearchTokenizer.printTokens(tokens, searchToken, lastToken); - return { tokens, searchToken, From aebee11884d2176a45cb17efa97bf3fdbc95449a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:55:43 -0600 Subject: [PATCH 111/185] Fix bug where labels with spaces weren't being escaped when selected --- .../javascripts/filtered_search/dropdown_non_user.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 0969df65836..84abaa920d6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -29,7 +29,6 @@ let escapedText = text; const hasSpace = text.indexOf(' ') !== -1; const hasDoubleQuote = text.indexOf('"') !== -1; - const hasSingleQuote = text.indexOf('\'') !== -1; // Encapsulate value with quotes if it has spaces // Known side effect: values's with both single and double quotes @@ -37,7 +36,8 @@ if (hasSpace) { if (hasDoubleQuote) { escapedText = `'${text}'`; - } else if (hasSingleQuote) { + } else { + // Encapsulate singleQuotes or if it hasSpace escapedText = `"${text}"`; } } From 262ad96aa9413e2ff1380930703e9e3a649bb855 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 10:55:55 -0600 Subject: [PATCH 112/185] Remove unnecessary function --- .../filtered_search/filtered_search_dropdown.js.es6 | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) 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 85d684e3058..a9dbb0f7ccb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -27,20 +27,12 @@ return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; } - getSelectedText(selectedToken) { - // TODO: Get last word from FilteredSearchTokenizer - const lastWord = this.input.value.split(' ').last(); - const lastWordIndex = selectedToken.indexOf(lastWord); - - return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); - } - itemClicked(e, getValueFunction) { const dataValueSet = this.setDataValueIfSelected(e.detail.selected); if (!dataValueSet) { - const value = getValueFunction(e.detail.selected) - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(value)); + const value = getValueFunction(e.detail.selected); + gl.FilteredSearchDropdownManager.addWordToInput(value); } this.dismissDropdown(); From 274f3e23e35ddf3116cd7c227b94ce68378c76af Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:06:45 -0600 Subject: [PATCH 113/185] Add comments to resolve todos --- .../filtered_search/filtered_search_manager.js.es6 | 6 +++--- 1 file changed, 3 insertions(+), 3 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 d3bccc4b14c..14e2e698f93 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -26,7 +26,7 @@ let conditionIndex = 0; const validCondition = gl.FilteredSearchTokenKeys.get() .filter(v => v.conditions && v.conditions.filter((c, index) => { - // TODO: Add comment here + // Return TokenKeys that have conditions that much the URL if (c.url === p) { conditionIndex = index; } @@ -155,8 +155,8 @@ const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); if (stateIndex !== -1) { - // TODO: Add comment here - const remaining = currentPath.slice(stateIndex + 6); + // Get currentState from url params if available + const remaining = currentPath.slice(stateIndex + 'state='.length); const separatorIndex = remaining.indexOf('&'); currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); From 8925c9604f95b66a9b8a4579e321961312bfc78d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:08:15 -0600 Subject: [PATCH 114/185] Add additional check before setting outerHTML --- app/assets/javascripts/droplab/droplab_ajax_filter.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index 6e1eb080e3b..c6c062d0886 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -78,7 +78,11 @@ require('../window')(function(w){ this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) { if (config.loadingTemplate && self.hook.list.data === undefined || self.hook.list.data.length === 0) { - self.hook.list.list.querySelector('[data-loading-template]').outerHTML = self.listTemplate; + const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } } if (!self.destroyed) { From 16e3fe3f15971bc34c48d65c902ce83a156e350d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 11:19:44 -0600 Subject: [PATCH 115/185] Fix missing method from refactoring --- .../javascripts/filtered_search/dropdown_hint.js.es6 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 43a0b1da0fe..1aef27163c6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -41,13 +41,20 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedText(token)); + gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedTextWithoutEscaping(token)); } this.dismissDropdown(); this.dispatchInputEvent(); } } + getSelectedTextWithoutEscaping(selectedToken) { + const lastWord = this.input.value.split(' ').last(); + const lastWordIndex = selectedToken.indexOf(lastWord); + + return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); + } + renderContent() { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); this.droplab.setData(this.hookId, dropdownData); From 6eafd748493e1125e1f5dea698dd3ca6affe15c5 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:25:31 -0600 Subject: [PATCH 116/185] Fix code styling issues --- .../filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search_dropdown.js.es6 | 9 +++++---- .../filtered_search_dropdown_manager.js.es6 | 20 +++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 1aef27163c6..a79779e4977 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -32,7 +32,7 @@ } itemClicked(e) { - const selected = e.detail.selected; + const { selected } = e.detail; if (selected.hasAttribute('data-value')) { this.dismissDropdown(); 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 a9dbb0f7ccb..130e6bba341 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -24,14 +24,15 @@ } getCurrentHook() { - return this.droplab.hooks.filter(h => h.id === this.hookId)[0]; + return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; } itemClicked(e, getValueFunction) { - const dataValueSet = this.setDataValueIfSelected(e.detail.selected); + const { selected } = e.detail; + const dataValueSet = this.setDataValueIfSelected(selected); if (!dataValueSet) { - const value = getValueFunction(e.detail.selected); + const value = getValueFunction(selected); gl.FilteredSearchDropdownManager.addWordToInput(value); } @@ -67,7 +68,7 @@ this.setAsDropdown(); const currentHook = this.getCurrentHook(); - const firstTimeInitialized = currentHook === undefined; + const firstTimeInitialized = currentHook === null; if (firstTimeInitialized || forceRenderContent) { this.renderContent(forceShowList); 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 a0764c275e5..59166840c50 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 @@ -55,7 +55,7 @@ } static addWordToInput(word, addSpace = false) { - const input = document.querySelector('.filtered-search') + const input = document.querySelector('.filtered-search'); const value = input.value; const hasExistingValue = value.length !== 0; const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value); @@ -88,22 +88,22 @@ } load(key, firstLoad = false) { - console.log(`🦄 load ${key} dropdown`); - const glClass = this.mapping[key].gl; - const element = this.mapping[key].element; + const mappingKey = this.mapping[key]; + const glClass = mappingKey.gl; + const element = mappingKey.element; let forceShowList = false; - if (!this.mapping[key].reference) { + if (!mappingKey.reference) { const dl = this.droplab; const defaultArguments = [null, dl, element, this.filteredSearchInput]; - const glArguments = defaultArguments.concat(this.mapping[key].extraArguments || []); + const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); // Passing glArguments to `new gl[glClass]()` - this.mapping[key].reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); } if (firstLoad) { - this.mapping[key].reference.init(); + mappingKey.reference.init(); } if (this.currentDropdown === 'hint') { @@ -112,7 +112,7 @@ } this.updateDropdownOffset(key); - this.mapping[key].reference.render(firstLoad, forceShowList); + mappingKey.reference.render(firstLoad, forceShowList); this.currentDropdown = key; } @@ -120,7 +120,7 @@ loadDropdown(dropdownName = '') { let firstLoad = false; - if(!this.droplab) { + if (!this.droplab) { firstLoad = true; this.droplab = new DropLab(); } From d8b8b9c88d0fbe4cfa1fae0796feaa82136cc747 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:27:10 -0600 Subject: [PATCH 117/185] Add support for delete key --- .../filtered_search/filtered_search_manager.js.es6 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 14e2e698f93..ebbd7e3129e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -125,7 +125,9 @@ } checkForBackspace(e) { - if (e.keyCode === 8) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { // Reposition dropdown so that it is aligned with cursor this.dropdownManager.updateCurrentDropdownOffset(); } From 214b6495ca1853c9653a0ff109e0163dbc1d1cb6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:28:17 -0600 Subject: [PATCH 118/185] Remove unnecessary stopPropagation --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 2 -- 1 file changed, 2 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 ebbd7e3129e..e068b5d2ebf 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -115,7 +115,6 @@ } clearSearch(e) { - e.stopPropagation(); e.preventDefault(); this.filteredSearchInput.value = ''; @@ -135,7 +134,6 @@ checkForEnter(e) { if (e.keyCode === 13) { - e.stopPropagation(); e.preventDefault(); // Prevent droplab from opening dropdown From f0935c4da5bcd84fac51f7a0d61ee0c8d8181679 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:29:51 -0600 Subject: [PATCH 119/185] Fix regex for + --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e068b5d2ebf..a89627384e9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -40,7 +40,7 @@ } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/[+]/g, ' ')) : value; + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; if (match) { From a30fbbddfb03a63305ff1bd273d7dd98c976936a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:31:22 -0600 Subject: [PATCH 120/185] Reduce over-verboseness --- .../filtered_search/filtered_search_manager.js.es6 | 6 ++---- 1 file changed, 2 insertions(+), 4 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 a89627384e9..77a9de96c8a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -47,13 +47,11 @@ const sanitizedKey = key.slice(0, key.indexOf('_')); const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; const symbol = match.symbol; - - const preferredQuotations = '"'; - let quotationsToUse = preferredQuotations; + let quotationsToUse; if (valueHasSpace) { // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf(preferredQuotations) === -1 ? preferredQuotations : '\''; + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; } inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; From 5116db243a2f1705462e792cbb71f666cfca98f0 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:34:01 -0600 Subject: [PATCH 121/185] Convert to single quotes --- app/assets/javascripts/lib/utils/text_utility.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index e47eccc3a33..db24bcf682b 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -27,8 +27,8 @@ * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 */ // re-use canvas object for better performance - var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement("canvas")); - var context = canvas.getContext("2d"); + var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); + var context = canvas.getContext('2d'); context.font = font; var metrics = context.measureText(text); return metrics.width; From 78fe37b169602d898ffbd756189706559aad84f2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:37:49 -0600 Subject: [PATCH 122/185] Move functions into class --- .../filtered_search_manager.js.es6 | 164 +++++++++--------- 1 file changed, 81 insertions(+), 83 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 77a9de96c8a..00b7dc195bb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,76 +1,5 @@ /* eslint-disable no-param-reassign */ ((global) => { - // TODO: Encapsulate inside class? - function toggleClearSearchButton(e) { - const clearSearchButton = document.querySelector('.clear-search'); - - if (e.target.value) { - clearSearchButton.classList.remove('hidden'); - } else { - clearSearchButton.classList.add('hidden'); - } - } - - function loadSearchParamsFromURL() { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - const params = window.location.search.slice(1).split('&'); - let inputValue = ''; - - params.forEach((p) => { - const split = p.split('='); - const key = decodeURIComponent(split[0]); - const value = split[1]; - - // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys.get() - let conditionIndex = 0; - const validCondition = gl.FilteredSearchTokenKeys.get() - .filter(v => v.conditions && v.conditions.filter((c, index) => { - // Return TokenKeys that have conditions that much the URL - if (c.url === p) { - conditionIndex = index; - } - return c.url === p; - })[0])[0]; - - if (validCondition) { - // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() - inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; - inputValue += ' '; - } else { - // Sanitize value since URL converts spaces into + - // Replace before decode so that we know what was originally + versus the encoded + - const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; - const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; - - if (match) { - const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; - const symbol = match.symbol; - let quotationsToUse; - - if (valueHasSpace) { - // Prefer ", but use ' if required - quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; - } - - inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; - inputValue += ' '; - } else if (!match && key === 'search') { - inputValue += sanitizedValue; - inputValue += ' '; - } - } - }); - - // Trim the last space value - document.querySelector('.filtered-search').value = inputValue.trim(); - - if (inputValue.trim()) { - document.querySelector('.clear-search').classList.remove('hidden'); - } - } - class FilteredSearchManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; @@ -79,7 +8,7 @@ this.dropdownManager = new gl.FilteredSearchDropdownManager(); this.bindEvents(); - loadSearchParamsFromURL(); + this.loadSearchParamsFromURL(); this.dropdownManager.setDropdown(); this.cleanupWrapper = this.cleanup.bind(this); @@ -93,12 +22,13 @@ bindEvents() { this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); + this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); this.checkForEnterWrapper = this.checkForEnter.bind(this); this.clearSearchWrapper = this.clearSearch.bind(this); this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.addEventListener('input', toggleClearSearchButton); + this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); @@ -106,21 +36,12 @@ unbindEvents() { this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); - this.filteredSearchInput.removeEventListener('input', toggleClearSearchButton); + this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); } - clearSearch(e) { - e.preventDefault(); - - this.filteredSearchInput.value = ''; - this.clearSearchButton.classList.add('hidden'); - - this.dropdownManager.resetDropdowns(); - } - checkForBackspace(e) { // 8 = Backspace Key // 46 = Delete Key @@ -141,6 +62,83 @@ } } + toggleClearSearchButton(e) { + if (e.target.value) { + this.clearSearchButton.classList.remove('hidden'); + } else { + this.clearSearchButton.classList.add('hidden'); + } + } + + clearSearch(e) { + e.preventDefault(); + + this.filteredSearchInput.value = ''; + this.clearSearchButton.classList.add('hidden'); + + this.dropdownManager.resetDropdowns(); + } + + loadSearchParamsFromURL() { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + const params = window.location.search.slice(1).split('&'); + let inputValue = ''; + + params.forEach((p) => { + const split = p.split('='); + const key = decodeURIComponent(split[0]); + const value = split[1]; + + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys.get() + let conditionIndex = 0; + const validCondition = gl.FilteredSearchTokenKeys.get() + .filter(v => v.conditions && v.conditions.filter((c, index) => { + // Return TokenKeys that have conditions that much the URL + if (c.url === p) { + conditionIndex = index; + } + return c.url === p; + })[0])[0]; + + if (validCondition) { + // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() + inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; + inputValue += ' '; + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; + const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; + + if (match) { + const sanitizedKey = key.slice(0, key.indexOf('_')); + const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; + const symbol = match.symbol; + let quotationsToUse; + + if (valueHasSpace) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + } + + inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; + inputValue += ' '; + } else if (!match && key === 'search') { + inputValue += sanitizedValue; + inputValue += ' '; + } + } + }); + + // Trim the last space value + this.filteredSearchInput.value = inputValue.trim(); + + if (inputValue.trim()) { + this.clearSearchButton.classList.remove('hidden'); + } + } + search() { let path = '?scope=all&utf8=✓'; From e9886b5704ae60a0e3517205de3958e9c0044a99 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 16:42:00 -0600 Subject: [PATCH 123/185] Convert string concatenations with an array join --- .../filtered_search_manager.js.es6 | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 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 00b7dc195bb..d0e39b6390d 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -83,7 +83,7 @@ // We can trust that each param has one & since values containing & will be encoded // Remove the first character of search as it is always ? const params = window.location.search.slice(1).split('&'); - let inputValue = ''; + let inputValues = []; params.forEach((p) => { const split = p.split('='); @@ -103,8 +103,7 @@ if (validCondition) { // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() - inputValue += `${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`; - inputValue += ' '; + inputValues.push(`${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`); } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + @@ -122,25 +121,23 @@ quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; } - inputValue += valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`; - inputValue += ' '; + inputValues.push(valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`); } else if (!match && key === 'search') { - inputValue += sanitizedValue; - inputValue += ' '; + inputValues.push(sanitizedValue); } } }); // Trim the last space value - this.filteredSearchInput.value = inputValue.trim(); + this.filteredSearchInput.value = inputValues.join(' '); - if (inputValue.trim()) { + if (inputValues.length > 0) { this.clearSearchButton.classList.remove('hidden'); } } search() { - let path = '?scope=all&utf8=✓'; + let paths = []; // Check current state const currentPath = window.location.search; @@ -158,7 +155,7 @@ currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); } - path += `&state=${currentState}`; + paths.push(`state=${currentState}`); tokens.forEach((token) => { const match = gl.FilteredSearchTokenKeys.get().filter(t => t.key === token.key)[0]; let tokenPath = ''; @@ -177,14 +174,14 @@ tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value)}`; } - path += `&${tokenPath}`; + paths.push(tokenPath); }); if (searchToken) { - path += `&search=${encodeURIComponent(searchToken)}`; + paths.push(`search=${encodeURIComponent(searchToken)}`); } - window.location = path; + window.location = `?scope=all&utf8=✓&${paths.join('&')}`; } } From 98cb6101ec45e8758a9e85a3a24fcde9803ece18 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:07:42 -0600 Subject: [PATCH 124/185] Refactor static data to get information from other variables instead --- app/assets/javascripts/filtered_search/dropdown_user.js.es6 | 4 ++-- .../filtered_search/filtered_search_dropdown.js.es6 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 8bc274e0b12..69b1ec3ea04 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -15,7 +15,7 @@ project_id: this.getProjectId(), current_user: true, }, - searchValueFunction: this.getSearchInput, + searchValueFunction: this.getSearchInput.bind(this), loadingTemplate: this.loadingTemplate, }, }; @@ -37,7 +37,7 @@ } getSearchInput() { - const query = document.querySelector('.filtered-search').value; + const query = this.input.value; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); const valueWithoutColon = value.slice(1); const hasPrefix = valueWithoutColon[0] === '@'; 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 130e6bba341..a5d8b0969c6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -5,7 +5,7 @@ class FilteredSearchDropdown { constructor(droplab, dropdown, input) { this.droplab = droplab; - this.hookId = 'filtered-search'; + this.hookId = input.getAttribute('data-id'); this.input = input; this.dropdown = dropdown; this.loadingTemplate = `
From 091a3e66e9f2616eefca0f3aba090063116629e2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:15:50 -0600 Subject: [PATCH 125/185] Add getParameterByName --- .../filtered_search_manager.js.es6 | 18 ++---------------- .../javascripts/lib/utils/common_utils.js.es6 | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 16 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 d0e39b6390d..2237a21ca60 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -138,24 +138,10 @@ search() { let paths = []; - - // Check current state - const currentPath = window.location.search; - const stateIndex = currentPath.indexOf('state='); - const defaultState = 'opened'; - let currentState = defaultState; - const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); - - if (stateIndex !== -1) { - // Get currentState from url params if available - const remaining = currentPath.slice(stateIndex + 'state='.length); - const separatorIndex = remaining.indexOf('&'); - - currentState = separatorIndex === -1 ? remaining : remaining.slice(0, separatorIndex); - } - + const currentState = gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); + tokens.forEach((token) => { const match = gl.FilteredSearchTokenKeys.get().filter(t => t.key === token.key)[0]; let tokenPath = ''; diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index b8d637a9827..f0186c1390f 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -124,6 +124,22 @@ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; }; + gl.utils.getParameterByName = function(name) { + var url = window.location.href; + var param = name.replace(/[[\]]/g, '\\$&'); + var regex = new RegExp(`[?&]${param}(=([^&#]*)|&|#|$)`); + var results = regex.exec(url); + + if (!results) { + return null; + } + + if (!results[2]) { + return ''; + } + return decodeURIComponent(results[2].replace(/\+/g, ' ')); + }; + gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; From bcb00bdc487ad0d0e95c4a46a7d9437dcefc4e33 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:19:16 -0600 Subject: [PATCH 126/185] Use turbolinks instead of window.location --- .../javascripts/filtered_search/filtered_search_manager.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2237a21ca60..e087d0fd45b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -167,7 +167,7 @@ paths.push(`search=${encodeURIComponent(searchToken)}`); } - window.location = `?scope=all&utf8=✓&${paths.join('&')}`; + Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); } } From 49231ccef2fb0bd0cd10d636864d1d50ea70cbdc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 21:24:55 -0600 Subject: [PATCH 127/185] Refactor getUrlParamsArray() --- .../filtered_search/filtered_search_manager.js.es6 | 4 +--- app/assets/javascripts/lib/utils/common_utils.js.es6 | 6 ++++++ 2 files changed, 7 insertions(+), 3 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 e087d0fd45b..3e57215d608 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -80,9 +80,7 @@ } loadSearchParamsFromURL() { - // We can trust that each param has one & since values containing & will be encoded - // Remove the first character of search as it is always ? - const params = window.location.search.slice(1).split('&'); + const params = gl.utils.getUrlParamsArray(); let inputValues = []; params.forEach((p) => { diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index f0186c1390f..7a18f760e1b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -124,6 +124,12 @@ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; }; + gl.utils.getUrlParamsArray = function () { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + return window.location.search.slice(1).split('&'); + } + gl.utils.getParameterByName = function(name) { var url = window.location.href; var param = name.replace(/[[\]]/g, '\\$&'); From bf16e91f2494912d44bc3a52d99ab36d3b33cd47 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 22:15:31 -0600 Subject: [PATCH 128/185] Refactor FilteredSearchTokenKeys model --- .../filtered_search_dropdown_manager.js.es6 | 2 +- .../filtered_search_manager.js.es6 | 48 ++++----- .../filtered_search_token_keys.js.es6 | 97 ++++++++++++------- .../filtered_search_tokenizer.es6 | 5 +- 4 files changed, 81 insertions(+), 71 deletions(-) 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 59166840c50..682857d1899 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 @@ -125,7 +125,7 @@ this.droplab = new DropLab(); } - const match = gl.FilteredSearchTokenKeys.get().filter(value => value.key === dropdownName.toLowerCase())[0]; + const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; 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 3e57215d608..d7fb3a0c204 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -85,42 +85,32 @@ params.forEach((p) => { const split = p.split('='); - const key = decodeURIComponent(split[0]); + const keyParam = decodeURIComponent(split[0]); const value = split[1]; - // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys.get() - let conditionIndex = 0; - const validCondition = gl.FilteredSearchTokenKeys.get() - .filter(v => v.conditions && v.conditions.filter((c, index) => { - // Return TokenKeys that have conditions that much the URL - if (c.url === p) { - conditionIndex = index; - } - return c.url === p; - })[0])[0]; + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); - if (validCondition) { - // Parse params based on rules provided in the conditions key of gl.FilteredSearchTokenKeys.get() - inputValues.push(`${validCondition.key}:${validCondition.conditions[conditionIndex].keyword}`); + if (condition) { + inputValues.push(`${condition.tokenKey}:${condition.value}`); } else { // Sanitize value since URL converts spaces into + // Replace before decode so that we know what was originally + versus the encoded + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; - const match = gl.FilteredSearchTokenKeys.get().filter(t => key === `${t.key}_${t.param}`)[0]; + const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); if (match) { - const sanitizedKey = key.slice(0, key.indexOf('_')); - const valueHasSpace = sanitizedValue.indexOf(' ') !== -1; + const sanitizedKey = keyParam.slice(0, keyParam.indexOf('_')); const symbol = match.symbol; - let quotationsToUse; + let quotationsToUse = ''; - if (valueHasSpace) { + if (sanitizedValue.indexOf(' ') !== -1) { // Prefer ", but use ' if required quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; } - inputValues.push(valueHasSpace ? `${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}` : `${sanitizedKey}:${symbol}${sanitizedValue}`); - } else if (!match && key === 'search') { + inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'search') { inputValues.push(sanitizedValue); } } @@ -141,21 +131,17 @@ paths.push(`state=${currentState}`); tokens.forEach((token) => { - const match = gl.FilteredSearchTokenKeys.get().filter(t => t.key === token.key)[0]; + const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); let tokenPath = ''; - if (token.wildcard && match.conditions) { - const condition = match.conditions - .filter(c => c.keyword === token.value.toLowerCase())[0]; - - if (condition) { - tokenPath = `${condition.url}`; - } + if (token.wildcard && condition) { + tokenPath = condition.url; } else if (!token.wildcard) { // Remove the wildcard token - tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value.slice(1))}`; + tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value.slice(1))}`; } else { - tokenPath = `${token.key}_${match.param}=${encodeURIComponent(token.value)}`; + tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value)}`; } paths.push(tokenPath); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 index 8d38a29a354..97eab6be8df 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -1,43 +1,68 @@ /* eslint-disable no-param-reassign */ ((global) => { + const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + }]; + + const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', + }, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', + }, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', + }]; + class FilteredSearchTokenKeys { static get() { - return [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - }, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - conditions: [{ - keyword: 'none', - url: 'assignee_id=0', - }], - }, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - conditions: [{ - keyword: 'none', - url: 'milestone_title=No+Milestone', - }, { - keyword: 'upcoming', - url: 'milestone_title=%23upcoming', - }], - }, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - conditions: [{ - keyword: 'none', - url: 'label_name[]=No+Label', - }], - }]; + return tokenKeys; + } + + static searchByKey(key) { + return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + } + + static searchBySymbol(symbol) { + return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + } + + static searchByKeyParam(keyParam) { + return tokenKeys.find(tokenKey => keyParam === `${tokenKey.key}_${tokenKey.param}`) || null; + } + + static searchByConditionUrl(url) { + return conditions.find(condition => condition.url === url) || null; + } + + static searchByConditionKeyValue(key, value) { + return conditions.find(condition => condition.tokenKey === key && condition.value === value) || null; } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index ac45d3b7986..365171252a1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -73,7 +73,6 @@ let tokens = []; let searchToken = ''; let lastToken = ''; - const validTokenKeys = gl.FilteredSearchTokenKeys.get(); const inputs = input.split(' '); let searchTerms = ''; @@ -107,8 +106,8 @@ if (colonIndex !== -1) { const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i); - const keyMatch = validTokenKeys.filter(v => v.key === tokenKey)[0]; - const symbolMatch = validTokenKeys.filter(v => v.symbol === tokenSymbol)[0]; + const keyMatch = gl.FilteredSearchTokenKeys.searchByKey(tokenKey); + const symbolMatch = gl.FilteredSearchTokenKeys.searchBySymbol(tokenSymbol); const doubleQuoteOccurrences = tokenValue.split('"').length - 1; const singleQuoteOccurrences = tokenValue.split('\'').length - 1; From a26cc6b25c2602fd4a47808b8c9c48dea789c6bf Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 12 Dec 2016 23:16:45 -0600 Subject: [PATCH 129/185] Simplify if else to make code easier to understand --- .../filtered_search/filtered_search_manager.js.es6 | 9 +++++---- 1 file changed, 5 insertions(+), 4 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 d7fb3a0c204..87bcbd272ca 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -137,11 +137,12 @@ if (token.wildcard && condition) { tokenPath = condition.url; - } else if (!token.wildcard) { - // Remove the wildcard token - tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value.slice(1))}`; - } else { + } else if (token.wildcard) { + // wildcard means that the token does not have a symbol tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value)}`; + } else { + // Remove the token symbol + tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value.slice(1))}`; } paths.push(tokenPath); From 27b2204009fb8fee409df013de013146bad1bfde Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 08:51:49 -0600 Subject: [PATCH 130/185] Convert hasOwnProperty check to if statement --- app/assets/javascripts/dispatcher.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 5a9ee5c7d78..9a76131b87f 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,7 +84,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if(gl.hasOwnProperty('FilteredSearchManager')) { + if(gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); From 6700b76bec2a3b6564bd9da12b580b998d767d30 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 08:52:43 -0600 Subject: [PATCH 131/185] Fix eslint --- .../filtered_search/dropdown_hint.js.es6 | 29 ++++++++--------- .../filtered_search/dropdown_non_user.js.es6 | 31 ++++++++++--------- .../filtered_search/dropdown_user.js.es6 | 14 ++++----- .../filtered_search_dropdown.js.es6 | 16 ++++++---- .../filtered_search_dropdown_manager.js.es6 | 24 +++++++------- .../filtered_search_manager.js.es6 | 15 ++++----- .../filtered_search_token_keys.js.es6 | 11 ++++--- .../filtered_search_tokenizer.es6 | 8 ++--- .../javascripts/lib/utils/common_utils.js.es6 | 2 +- .../javascripts/lib/utils/text_utility.js | 3 +- 10 files changed, 81 insertions(+), 72 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index a79779e4977..b920b17d915 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -1,20 +1,18 @@ -/* eslint-disable no-param-reassign */ /*= require filtered_search/filtered_search_dropdown */ - -((global) => { +(() => { const dropdownData = [{ icon: 'fa-pencil', hint: 'author:', - tag: '<author>' - },{ + tag: '<author>', + }, { icon: 'fa-user', hint: 'assignee:', tag: '<assignee>', - },{ + }, { icon: 'fa-clock-o', hint: 'milestone:', tag: '<milestone>', - },{ + }, { icon: 'fa-tag', hint: 'label:', tag: '<label>', @@ -27,7 +25,7 @@ droplabFilter: { template: 'hint', filterFunction: this.filterMethod, - } + }, }; } @@ -41,7 +39,8 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchDropdownManager.addWordToInput(this.getSelectedTextWithoutEscaping(token)); + gl.FilteredSearchDropdownManager + .addWordToInput(this.getSelectedTextWithoutEscaping(token)); } this.dismissDropdown(); this.dispatchInputEvent(); @@ -61,15 +60,16 @@ } filterMethod(item, query) { + const updatedItem = item; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); if (value === '') { - item.droplab_hidden = false; + updatedItem.droplab_hidden = false; } else { - item.droplab_hidden = item['hint'].indexOf(value) === -1; + updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1; } - return item; + return updatedItem; } init() { @@ -77,5 +77,6 @@ } } - global.DropdownHint = DropdownHint; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.DropdownHint = DropdownHint; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 84abaa920d6..95133db4c04 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -1,26 +1,24 @@ -/* eslint-disable no-param-reassign */ /*= require filtered_search/filtered_search_dropdown */ - -((global) => { +(() => { class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, endpoint, symbol) { super(droplab, dropdown, input); this.symbol = symbol; this.config = { droplabAjax: { - endpoint: endpoint, + endpoint, method: 'setData', loadingTemplate: this.loadingTemplate, }, droplabFilter: { filterFunction: this.filterWithSymbol.bind(this, this.symbol), - } + }, }; } itemClicked(e) { super.itemClicked(e, (selected) => { - const title = e.detail.selected.querySelector('.js-data-value').innerText.trim(); + const title = selected.querySelector('.js-data-value').innerText.trim(); return `${this.symbol}${this.getEscapedText(title)}`; }); } @@ -46,30 +44,35 @@ } filterWithSymbol(filterSymbol, item, query) { + const updatedItem = item; const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); const valueWithoutColon = value.slice(1).toLowerCase(); const prefix = valueWithoutColon[0]; const valueWithoutPrefix = valueWithoutColon.slice(1); - const title = item.title.toLowerCase(); + const title = updatedItem.title.toLowerCase(); // Eg. filterSymbol = ~ for labels - const matchWithoutPrefix = prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + const matchWithoutPrefix = + prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; const match = title.indexOf(valueWithoutColon) !== -1; - item.droplab_hidden = !match && !matchWithoutPrefix; - return item; + updatedItem.droplab_hidden = !match && !matchWithoutPrefix; + return updatedItem; } renderContent(forceShowList = false) { - this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + this.droplab + .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); super.renderContent(forceShowList); } init() { - this.droplab.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); + this.droplab + .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); } } - global.DropdownNonUser = DropdownNonUser; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.DropdownNonUser = DropdownNonUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 69b1ec3ea04..2ee46559e63 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -1,7 +1,5 @@ -/* eslint-disable no-param-reassign */ /*= require filtered_search/filtered_search_dropdown */ - -((global) => { +(() => { class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); @@ -22,9 +20,8 @@ } itemClicked(e) { - super.itemClicked(e, (selected) => { - return selected.querySelector('.dropdown-light-content').innerText.trim(); - }); + super.itemClicked(e, + selected => selected.querySelector('.dropdown-light-content').innerText.trim()); } renderContent(forceShowList = false) { @@ -51,5 +48,6 @@ } } - global.DropdownUser = DropdownUser; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.DropdownUser = DropdownUser; +})(); 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 a5d8b0969c6..7ddfdca10fa 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; class FilteredSearchDropdown { @@ -72,7 +71,7 @@ if (firstTimeInitialized || forceRenderContent) { this.renderContent(forceShowList); - } else if(currentHook.list.list.id !== this.dropdown.id) { + } else if (currentHook.list.list.id !== this.dropdown.id) { this.renderContent(forceShowList); } } @@ -96,10 +95,15 @@ resetFilters() { const hook = this.getCurrentHook(); const data = hook.list.data; - const results = data.map(o => o.droplab_hidden = false); + const results = data.map((o) => { + const updated = o; + updated.droplab_hidden = false; + return updated; + }); hook.list.render(results); } } - global.FilteredSearchDropdown = FilteredSearchDropdown; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchDropdown = FilteredSearchDropdown; +})(); 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 682857d1899..7864ebf7aa1 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 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { class FilteredSearchDropdownManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; @@ -51,7 +50,7 @@ gl: 'DropdownHint', element: document.querySelector('#js-dropdown-hint'), }, - } + }; } static addWordToInput(word, addSpace = false) { @@ -60,7 +59,7 @@ const hasExistingValue = value.length !== 0; const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value); - if (lastToken.hasOwnProperty('key')) { + if ({}.hasOwnProperty.call(lastToken, 'key')) { // Spaces inside the token means that the token value will be escaped by quotes const hasQuotes = lastToken.value.indexOf(' ') !== -1; @@ -82,7 +81,8 @@ } const filterIconPadding = 27; - const offset = gl.text.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; + const offset = gl.text + .getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding; this.mapping[key].reference.setOffset(offset); } @@ -99,7 +99,7 @@ const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); // Passing glArguments to `new gl[glClass]()` - mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments)); + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); } if (firstLoad) { @@ -126,12 +126,13 @@ } const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); - const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key && this.mapping.hasOwnProperty(match.key); + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key + && {}.hasOwnProperty.call(this.mapping, match.key); const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { // `hint` is not listed as a tokenKey (since it is not a real `filter`) - const key = match && match.hasOwnProperty('key') ? match.key : 'hint'; + const key = match && {}.hasOwnProperty.call(match, 'key') ? match.key : 'hint'; this.load(key, firstLoad); } @@ -146,7 +147,7 @@ // Eg. token = 'label:' const { tokenKey } = this.tokenizer.parseToken(lastToken); this.loadDropdown(tokenKey); - } else if (lastToken.hasOwnProperty('key')) { + } else if ({}.hasOwnProperty.call(lastToken, 'key')) { // Token has been initialized into an object because it has a value this.loadDropdown(lastToken.key); } else { @@ -173,5 +174,6 @@ } } - global.FilteredSearchDropdownManager = FilteredSearchDropdownManager; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(); 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 87bcbd272ca..96131a673ef 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { class FilteredSearchManager { constructor() { this.tokenizer = gl.FilteredSearchTokenizer; @@ -81,7 +80,7 @@ loadSearchParamsFromURL() { const params = gl.utils.getUrlParamsArray(); - let inputValues = []; + const inputValues = []; params.forEach((p) => { const split = p.split('='); @@ -125,13 +124,14 @@ } search() { - let paths = []; + const paths = []; const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); const currentState = gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); tokens.forEach((token) => { - const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const condition = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(token.key, token.value.toLowerCase()); const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); let tokenPath = ''; @@ -156,5 +156,6 @@ } } - global.FilteredSearchManager = FilteredSearchManager; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchManager = FilteredSearchManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 index 97eab6be8df..a1830d13e5f 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { const tokenKeys = [{ key: 'author', type: 'string', @@ -62,9 +61,11 @@ } static searchByConditionKeyValue(key, value) { - return conditions.find(condition => condition.tokenKey === key && condition.value === value) || null; + return conditions + .find(condition => condition.tokenKey === key && condition.value === value) || null; } } - global.FilteredSearchTokenKeys = FilteredSearchTokenKeys; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 index 365171252a1..0507f7bbc48 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 @@ -1,5 +1,4 @@ -/* eslint-disable no-param-reassign */ -((global) => { +(() => { class FilteredSearchTokenizer { static parseToken(input) { const colonIndex = input.indexOf(':'); @@ -161,5 +160,6 @@ } } - global.FilteredSearchTokenizer = FilteredSearchTokenizer; -})(window.gl || (window.gl = {})); + window.gl = window.gl || {}; + gl.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 7a18f760e1b..9f1a62bf8b1 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -128,7 +128,7 @@ // We can trust that each param has one & since values containing & will be encoded // Remove the first character of search as it is always ? return window.location.search.slice(1).split('&'); - } + }; gl.utils.getParameterByName = function(name) { var url = window.location.href; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index db24bcf682b..c856a26ae40 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -30,8 +30,7 @@ var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); var context = canvas.getContext('2d'); context.font = font; - var metrics = context.measureText(text); - return metrics.width; + return context.measureText(text).width; }; gl.text.selectedText = function(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); From 3d18319e0deae15836e994088f1254b28015d188 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 09:52:23 -0600 Subject: [PATCH 132/185] Rename to .js.es6 --- ...ered_search_tokenizer.es6 => filtered_search_tokenizer.js.es6} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/filtered_search/{filtered_search_tokenizer.es6 => filtered_search_tokenizer.js.es6} (100%) diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 similarity index 100% rename from app/assets/javascripts/filtered_search/filtered_search_tokenizer.es6 rename to app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 From 4786a9780337839844d5839fefda51430e13685e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 21:36:54 -0600 Subject: [PATCH 133/185] Fix es6 errors --- app/assets/javascripts/droplab/droplab.js | 101 ++++++++++++------ .../droplab/droplab_ajax_filter.js | 2 +- .../javascripts/lib/utils/common_utils.js.es6 | 2 +- 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 359cd82bbcd..94236153e41 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -1,3 +1,29 @@ +// Determine where to place this +if (typeof Object.assign != 'function') { + Object.assign = function (target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} + /* eslint-disable */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Tue, 13 Dec 2016 21:55:25 -0600 Subject: [PATCH 134/185] Fix eslint --- app/assets/javascripts/droplab/droplab.js | 2 +- .../filtered_search/dropdown_hint.js.es6 | 18 ++------ .../filtered_search/dropdown_non_user.js.es6 | 46 +++---------------- .../filtered_search/dropdown_user.js.es6 | 3 ++ .../filtered_search_dropdown.js.es6 | 13 +----- .../filtered_search_dropdown_manager.js.es6 | 2 + .../filtered_search_manager.js.es6 | 2 + .../filtered_search_tokenizer.js.es6 | 32 +++++++------ 8 files changed, 37 insertions(+), 81 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js index 94236153e41..ed545ec8748 100644 --- a/app/assets/javascripts/droplab/droplab.js +++ b/app/assets/javascripts/droplab/droplab.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Determine where to place this if (typeof Object.assign != 'function') { Object.assign = function (target, varArgs) { // .length of function is 2 @@ -24,7 +25,6 @@ if (typeof Object.assign != 'function') { }; } -/* eslint-disable */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o { const dropdownData = [{ icon: 'fa-pencil', @@ -24,7 +27,7 @@ this.config = { droplabFilter: { template: 'hint', - filterFunction: this.filterMethod, + filterFunction: gl.DropdownUtils.filterMethod, }, }; } @@ -59,19 +62,6 @@ this.droplab.setData(this.hookId, dropdownData); } - filterMethod(item, query) { - const updatedItem = item; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - - if (value === '') { - updatedItem.droplab_hidden = false; - } else { - updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1; - } - - return updatedItem; - } - init() { this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); } diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 95133db4c04..54090375c5c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -1,4 +1,8 @@ /*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjax */ +/* global droplabFilter */ + (() => { class DropdownNonUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input, endpoint, symbol) { @@ -11,7 +15,7 @@ loadingTemplate: this.loadingTemplate, }, droplabFilter: { - filterFunction: this.filterWithSymbol.bind(this, this.symbol), + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol), }, }; } @@ -19,48 +23,10 @@ itemClicked(e) { super.itemClicked(e, (selected) => { const title = selected.querySelector('.js-data-value').innerText.trim(); - return `${this.symbol}${this.getEscapedText(title)}`; + return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; }); } - getEscapedText(text) { - let escapedText = text; - const hasSpace = text.indexOf(' ') !== -1; - const hasDoubleQuote = text.indexOf('"') !== -1; - - // Encapsulate value with quotes if it has spaces - // Known side effect: values's with both single and double quotes - // won't escape properly - if (hasSpace) { - if (hasDoubleQuote) { - escapedText = `'${text}'`; - } else { - // Encapsulate singleQuotes or if it hasSpace - escapedText = `"${text}"`; - } - } - - return escapedText; - } - - filterWithSymbol(filterSymbol, item, query) { - const updatedItem = item; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const prefix = valueWithoutColon[0]; - const valueWithoutPrefix = valueWithoutColon.slice(1); - - const title = updatedItem.title.toLowerCase(); - - // Eg. filterSymbol = ~ for labels - const matchWithoutPrefix = - prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; - const match = title.indexOf(valueWithoutColon) !== -1; - - updatedItem.droplab_hidden = !match && !matchWithoutPrefix; - return updatedItem; - } - renderContent(forceShowList = false) { this.droplab .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 2ee46559e63..7a566907312 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -1,4 +1,7 @@ /*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjaxFilter */ + (() => { class DropdownUser extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { 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 7ddfdca10fa..6c66a3b0613 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -28,7 +28,7 @@ itemClicked(e, getValueFunction) { const { selected } = e.detail; - const dataValueSet = this.setDataValueIfSelected(selected); + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); if (!dataValueSet) { const value = getValueFunction(selected); @@ -46,17 +46,6 @@ this.dropdown.style.left = `${offset}px`; } - setDataValueIfSelected(selected) { - const dataValue = selected.getAttribute('data-value'); - - if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(dataValue); - } - - // Return boolean based on whether it was set - return dataValue !== null; - } - renderContent(forceShowList = false) { if (forceShowList && this.getCurrentHook().list.hidden) { this.getCurrentHook().list.show(); 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 7864ebf7aa1..ac71b5e4434 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 @@ -1,3 +1,5 @@ +/* global DropLab */ + (() => { class FilteredSearchDropdownManager { constructor() { 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 96131a673ef..e5b37f1e691 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,3 +1,5 @@ +/* global Turbolinks */ + (() => { class FilteredSearchManager { constructor() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index 0507f7bbc48..57c0e8fc359 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -16,7 +16,7 @@ tokenKey, tokenValue, tokenSymbol, - } + }; } static getLastTokenObject(input) { @@ -29,7 +29,7 @@ return { key, value, - } + }; } static getLastToken(input) { @@ -40,19 +40,19 @@ const doubleQuote = '"'; const singleQuote = '\''; - while(!completeToken && i >= 0) { + while (!completeToken && i >= 0) { const isDoubleQuote = input[i] === doubleQuote; const isSingleQuote = input[i] === singleQuote; // If the second quotation is found - if ((lastQuotation === doubleQuote && input[i] === doubleQuote) || - (lastQuotation === singleQuote && input[i] === singleQuote)) { + if ((lastQuotation === doubleQuote && isDoubleQuote) || + (lastQuotation === singleQuote && isSingleQuote)) { completeQuotation = true; } // Save the first quotation - if ((input[i] === doubleQuote && lastQuotation === '') || - (input[i] === singleQuote && lastQuotation === '')) { + if ((isDoubleQuote && lastQuotation === '') || + (isSingleQuote && lastQuotation === '')) { lastQuotation = input[i]; completeQuotation = false; } @@ -60,7 +60,7 @@ if (completeQuotation && input[i] === ' ') { completeToken = true; } else { - i--; + i -= 1; } } @@ -69,7 +69,7 @@ } static processTokens(input) { - let tokens = []; + const tokens = []; let searchToken = ''; let lastToken = ''; @@ -118,16 +118,20 @@ const singleQuoteExist = singleQuoteIndex !== -1; const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist; - const doubleQuoteIsBeforeSingleQuote = doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; + const doubleQuoteIsBeforeSingleQuote = + doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist; - const singleQuoteIsBeforeDoubleQuote = doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; + const singleQuoteIsBeforeDoubleQuote = + doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; - if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) && doubleQuoteOccurrences % 2 !== 0) { + if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) + && doubleQuoteOccurrences % 2 !== 0) { // " is found and is in front of ' (if any) lastQuotation = '"'; incompleteToken = true; - } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) && singleQuoteOccurrences % 2 !== 0) { + } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) + && singleQuoteOccurrences % 2 !== 0) { // ' is found and is in front of " (if any) lastQuotation = '\''; incompleteToken = true; @@ -137,7 +141,7 @@ tokens.push({ key: keyMatch.key, value: tokenValue, - wildcard: symbolMatch ? false : true, + wildcard: !symbolMatch, }); lastToken = tokens.last(); From 2461b9b635501fc9ae98d246e0dd6d23af555351 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 21:59:28 -0600 Subject: [PATCH 135/185] Fix scss lint --- app/assets/stylesheets/framework/filters.scss | 8 ++++---- app/assets/stylesheets/framework/variables.scss | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index b6c137d647a..dbe94813a93 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -39,7 +39,7 @@ padding-right: 25px; &:focus ~ .fa-filter { - color: #444; + color: $common-gray-dark; } } @@ -65,7 +65,7 @@ outline: none; &:hover .fa-times { - color: #444; + color: $common-gray-dark; } } } @@ -92,11 +92,11 @@ &:hover, &:focus { background-color: $dropdown-hover-color; - color: white; + color: $white-light; text-decoration: none; .dropdown-label-box { - border-color: white; + border-color: $white-light; border-style: solid; border-width: 2px; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f3cb3d33d99..cf9424ea5dd 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -266,7 +266,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%); /* * Filtered Search */ -$dropdown-hover-color: #3B86FF; +$dropdown-hover-color: #3b86ff; /* * Buttons From 1f7659912ca73f6774c4f1b66ad4e5e48cc51068 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 13 Dec 2016 23:30:28 -0600 Subject: [PATCH 136/185] Add jasmine tests to dropdown utils --- .../filtered_search/dropdown_utils.js.es6 | 68 ++++++++++ .../dropdown_utils_spec.js.es6 | 121 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 app/assets/javascripts/filtered_search/dropdown_utils.js.es6 create mode 100644 spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 new file mode 100644 index 00000000000..3837b020fd3 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -0,0 +1,68 @@ +(() => { + class DropdownUtils { + static getEscapedText(text) { + let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + + // Encapsulate value with quotes if it has spaces + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { + escapedText = `'${text}'`; + } else { + // Encapsulate singleQuotes or if it hasSpace + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + static filterWithSymbol(filterSymbol, item, query) { + const updatedItem = item; + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const valueWithoutColon = value.slice(1).toLowerCase(); + const prefix = valueWithoutColon[0]; + const valueWithoutPrefix = valueWithoutColon.slice(1); + + const title = updatedItem.title.toLowerCase(); + + // Eg. filterSymbol = ~ for labels + const matchWithoutPrefix = + prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; + const match = title.indexOf(valueWithoutColon) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutPrefix; + return updatedItem; + } + + static filterMethod(item, query) { + const updatedItem = item; + const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + + if (value === '') { + updatedItem.droplab_hidden = false; + } else { + updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1; + } + + return updatedItem; + } + + static setDataValueIfSelected(selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchDropdownManager.addWordToInput(dataValue); + } + + // Return boolean based on whether it was set + return dataValue !== null; + } + } + + window.gl = window.gl || {}; + gl.DropdownUtils = DropdownUtils; +})(); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 new file mode 100644 index 00000000000..07293b9f877 --- /dev/null +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -0,0 +1,121 @@ +//= require filtered_search/dropdown_utils +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown_manager + +(() => { + describe('Dropdown Utils', () => { + describe('getEscapedText', () => { + it('should return same word when it has no space', () => { + const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); + expect(escaped).toBe('textWithoutSpace'); + }); + + it('should escape with double quotes', () => { + let escaped = gl.DropdownUtils.getEscapedText('text with space'); + expect(escaped).toBe('"text with space"'); + + escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); + expect(escaped).toBe('"won\'t fix"'); + }); + + it('should escape with single quotes', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); + expect(escaped).toBe('\'won"t fix\''); + }); + + it('should escape with single quotes by default', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); + expect(escaped).toBe('\'won"t\' fix\''); + }); + }); + + describe('filterWithSymbol', () => { + const item = { + title: '@root', + }; + + beforeEach(() => { + spyOn(gl.FilteredSearchTokenizer, 'getLastTokenObject') + .and.callFake(query => ({ value: query })); + }); + + it('should filter without symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should filter with symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':@roo'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should filter with invalid symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':#'); + expect(updatedItem.droplab_hidden).toBe(true); + }); + + it('should filter with colon', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + }); + + describe('filterMethod', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchTokenizer, 'getLastTokenObject') + .and.callFake(query => ({ value: query })); + }); + + it('should filter by hint', () => { + let updatedItem = gl.DropdownUtils.filterMethod({ + hint: 'label', + }, 'l'); + expect(updatedItem.droplab_hidden).toBe(false); + + updatedItem = gl.DropdownUtils.filterMethod({ + hint: 'label', + }, 'o'); + expect(updatedItem.droplab_hidden).toBe(true); + }); + + it('should return droplab_hidden false when item has no hint', () => { + const updatedItem = gl.DropdownUtils.filterMethod({}, ''); + expect(updatedItem.droplab_hidden).toBe(false); + }); + }); + + describe('setDataValueIfSelected', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') + .and.callFake(() => {}); + }); + + it('calls addWordToInput when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + gl.DropdownUtils.setDataValueIfSelected(selected); + expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + }); + + it('returns true when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(selected); + expect(result).toBe(true); + }); + + it('returns false when dataValue does not exist', () => { + const selected = { + getAttribute: () => null, + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(selected); + expect(result).toBe(false); + }); + }); + }); +})(); From 9408693d30cc2af7059cff2c9ddc503a92db86a6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 13:39:03 -0600 Subject: [PATCH 137/185] Add webkit to flex --- app/assets/stylesheets/framework/filters.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index dbe94813a93..e47511940a7 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -109,11 +109,14 @@ } .dropdown-user { + display: -webkit-flex; display: flex; } .dropdown-user-details { + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; } } From cf391760f19943af59ac43495a91db4126dbeb8d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 13:52:11 -0600 Subject: [PATCH 138/185] Fix HAML attributes --- .../shared/issuable/_search_bar.html.haml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 335552c0a26..dbef87e67cf 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -18,7 +18,7 @@ = icon('times') #js-dropdown-hint.dropdown-menu.hint-dropdown %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value': '' } + %li.filter-dropdown-item{ 'data-value' => '' } %button.btn.btn-link = icon('search') %span @@ -26,7 +26,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %i.fa{ 'class': '{{icon}}'} + %i.fa{ class: '{{icon}}'} %span.js-filter-hint {{hint}} %span.js-filter-tag.dropdown-light-content @@ -35,7 +35,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', width: '30' } .dropdown-user-details %span {{name}} @@ -43,14 +43,14 @@ @{{username}} #js-dropdown-assignee.dropdown-menu %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value': 'none' } + %li.filter-dropdown-item{ 'data-value' => 'none' } %button.btn.btn-link No Assignee %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src': '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', width: '30' } .dropdown-user-details %span {{name}} @@ -58,10 +58,10 @@ @{{username}} #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value': 'none' } + %li.filter-dropdown-item{ 'data-value' => 'none' } %button.btn.btn-link No Milestone - %li.filter-dropdown-item{ 'data-value': 'upcoming' } + %li.filter-dropdown-item{ 'data-value' => 'upcoming' } %button.btn.btn-link Upcoming %li.divider @@ -71,14 +71,14 @@ {{title}} #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } %ul{ 'data-dropdown' => true } - %li.filter-dropdown-item{ 'data-value': 'none' } + %li.filter-dropdown-item{ 'data-value' => 'none' } %button.btn.btn-link No Label %li.divider %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %span.dropdown-label-box{ 'style': 'background: {{color}}'} + %span.dropdown-label-box{ style => 'background: {{color}}'} %span.label-title.js-data-value {{title}} .pull-right From 3cb156dd45e7a83d83c59094894e015386d4caea Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 14:17:28 -0600 Subject: [PATCH 139/185] Add tests for new common_utils functions --- .../lib/utils/common_utils_spec.js.es6 | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index ef75f600898..4ba83d235c4 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -15,6 +15,7 @@ expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); }); }); + describe('gl.utils.parseUrlPathname', () => { beforeEach(() => { spyOn(gl.utils, 'parseUrl').and.callFake(url => ({ @@ -28,5 +29,29 @@ expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); }); }); + + describe('gl.utils.getUrlParamsArray', () => { + it('should return params array', () => { + expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true); + }); + + it('should remove the question mark from the search params', () => { + const paramsArray = gl.utils.getUrlParamsArray(); + expect(paramsArray[0][0] !== '?').toBe(true); + }); + }); + + describe('gl.utils.getParameterByName', () => { + it('should return valid parameter', () => { + const value = gl.utils.getParameterByName('reporter'); + expect(value).toBe('Console'); + }); + + it('should return invalid parameter', () => { + const value = gl.utils.getParameterByName('fakeParameter'); + expect(value).toBe(null); + }); + }); + }); })(); From 4577f1f1749976412ce03941ef712e763c9d618a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 14:37:17 -0600 Subject: [PATCH 140/185] Add text utility spec --- .../lib/utils/common_utils_spec.js.es6 | 1 - .../lib/utils/text_utility_spec.js.es6 | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 spec/javascripts/lib/utils/text_utility_spec.js.es6 diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 4ba83d235c4..031f9ca03c9 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -52,6 +52,5 @@ expect(value).toBe(null); }); }); - }); })(); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 new file mode 100644 index 00000000000..e97356b65d5 --- /dev/null +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -0,0 +1,25 @@ +//= require lib/utils/text_utility + +(() => { + describe('text_utility', () => { + describe('gl.text.getTextWidth', () => { + it('returns zero width when no text is passed', () => { + expect(gl.text.getTextWidth('')).toBe(0); + }); + + it('returns zero width when no text is passed and font is passed', () => { + expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); + }); + + it('returns width when text is passed', () => { + expect(gl.text.getTextWidth('foo') > 0).toBe(true); + }); + + it('returns bigger width when font is larger', () => { + const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); + const regular = gl.text.getTextWidth('foo', '10px sans-serif'); + expect(largeFont > regular).toBe(true); + }); + }); + }); +})(); From d93ccb8e0949e345efa9a1dcf874c73f8d1975bc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 15:34:04 -0600 Subject: [PATCH 141/185] Fix invalid style attribute operator --- app/views/shared/issuable/_search_bar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index dbef87e67cf..aca39941381 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -78,7 +78,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %span.dropdown-label-box{ style => 'background: {{color}}'} + %span.dropdown-label-box{ style: 'background: {{color}}'} %span.label-title.js-data-value {{title}} .pull-right From f7b6361124c3abb4380035eaf3b7625433686543 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 14 Dec 2016 21:51:33 -0600 Subject: [PATCH 142/185] Add jasmine tests for filtered search dropdown manager --- ...ltered_search_dropdown_manager_spec.js.es6 | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 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 new file mode 100644 index 00000000000..11765d7d7ea --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -0,0 +1,57 @@ +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown_manager + +(() => { + describe('Filtered Search Dropdown Manager', () => { + describe('addWordToInput', () => { + describe('add word and when lastToken is an empty object', () => { + function getInput() { + return document.querySelector('.filtered-search'); + } + + beforeEach(() => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens') + .and.callFake(query => ({ + lastToken: {} + }) + ); + + const input = document.createElement('input'); + input.classList.add('filtered-search'); + document.body.appendChild(input); + + expect(input.value).toBe(''); + }); + + afterEach(() => { + document.querySelector('.filtered-search').outerHTML = ''; + }); + + it('should add word', () => { + gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); + expect(getInput().value).toBe('firstWord'); + }); + + it('should not add space before first word', () => { + gl.FilteredSearchDropdownManager.addWordToInput('firstWord', true); + expect(getInput().value).toBe('firstWord'); + }); + + it('should not add space before second word by default', () => { + gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); + expect(getInput().value).toBe('firstWord'); + gl.FilteredSearchDropdownManager.addWordToInput('secondWord'); + expect(getInput().value).toBe('firstWordsecondWord'); + }); + + it('should add space before new word when addSpace is passed', () => { + expect(getInput().value).toBe(''); + gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); + expect(getInput().value).toBe('firstWord'); + gl.FilteredSearchDropdownManager.addWordToInput('secondWord', true); + expect(getInput().value).toBe('firstWord secondWord'); + }); + }); + }); + }); +})(); From 410e223597c78df30ddcf71a859b65ea05729794 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 08:34:34 -0600 Subject: [PATCH 143/185] Fix search autocomplete jasmine test --- spec/javascripts/search_autocomplete_spec.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index e13c4ad772c..2d3f44e7980 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -11,6 +11,7 @@ (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; + var userName = 'root'; widget = null; @@ -19,6 +20,7 @@ window.gon || (window.gon = {}); window.gon.current_user_id = userId; + window.gon.current_username = userName; dashboardIssuesPath = '/dashboard/issues'; @@ -93,8 +95,8 @@ assertLinks = function(list, issuesPath, mrsPath) { var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; - issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId; - issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId; + issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName; + issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName; mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; a1 = "a[href='" + issuesAssignedToMeLink + "']"; From 6662f034be3d576fba32d0a0acdc2cb95145a761 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 08:40:25 -0600 Subject: [PATCH 144/185] Fix eslint --- .../filtered_search_dropdown_manager_spec.js.es6 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 11765d7d7ea..4a358bd43e3 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 @@ -11,10 +11,9 @@ beforeEach(() => { spyOn(gl.FilteredSearchTokenizer, 'processTokens') - .and.callFake(query => ({ - lastToken: {} - }) - ); + .and.callFake(() => ({ + lastToken: {}, + })); const input = document.createElement('input'); input.classList.add('filtered-search'); From 657ac981acdcd2b070d838e530a2620c4db8bf04 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 09:07:34 -0600 Subject: [PATCH 145/185] Fix spinach tests --- app/assets/javascripts/dispatcher.js.es6 | 2 +- features/project/issues/filter_labels.feature | 28 ---------- features/project/issues/issues.feature | 56 ------------------- 3 files changed, 1 insertion(+), 85 deletions(-) delete mode 100644 features/project/issues/filter_labels.feature diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 9a76131b87f..1e9111f4718 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,7 +84,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if(gl.FilteredSearchManager) { + if(document.querySelector('.filtered-search') && gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature deleted file mode 100644 index 49d7a3b9af2..00000000000 --- a/features/project/issues/filter_labels.feature +++ /dev/null @@ -1,28 +0,0 @@ -@project_issues -Feature: Project Issues Filter Labels - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has labels: "bug", "feature", "enhancement" - And project "Shop" has issue "Bugfix1" with labels: "bug", "feature" - And project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement" - And project "Shop" has issue "Feature1" with labels: "feature" - Given I visit project "Shop" issues page - - @javascript - Scenario: I filter by one label - Given I click link "bug" - And I click "dropdown close button" - Then I should see "Bugfix1" in issues list - And I should see "Bugfix2" in issues list - And I should not see "Feature1" in issues list - - # TODO: make labels filter works according to this scanario - # right now it looks for label 1 OR label 2. Old behaviour (this test) was - # all issues that have both label 1 AND label 2 - #Scenario: I filter by two labels - #Given I click link "bug" - #And I click link "feature" - #Then I should see "Bugfix1" in issues list - #And I should not see "Bugfix2" in issues list - #And I should not see "Feature1" in issues list diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 80670063ea0..b2b4fe72220 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -26,12 +26,6 @@ Feature: Project Issues Given I click link "Release 0.4" Then I should see issue "Release 0.4" - @javascript - Scenario: I filter by author - Given I add a user to project "Shop" - And I click "author" dropdown - Then I see current user as the first user - Scenario: I submit new unassigned issue Given I click link "New Issue" And I submit new issue "500 error on profile" @@ -84,56 +78,6 @@ Feature: Project Issues And I sort the list by "Least popular" Then The list should be sorted by "Least popular" - @javascript - Scenario: I search issue - Given I fill in issue search with "Re" - Then I should see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: I search issue that not exist - Given I fill in issue search with "Bu" - Then I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - - @javascript - Scenario: I search all issues - Given I click link "All" - And I fill in issue search with ".3" - Then I should see "Release 0.3" in issues - And I should not see "Release 0.4" in issues - - @javascript - Scenario: Search issues when search string exactly matches issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And I fill in issue search with 'Description for issue1' - Then I should see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: Search issues when search string partially matches issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And project 'Shop' has issue 'Feature1' with description: 'Feature submitted for issue1' - And I fill in issue search with 'issue1' - Then I should see 'Feature1' in issues - Then I should see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - @javascript - Scenario: Search issues when search string matches no issue description - Given project 'Shop' has issue 'Bugfix1' with description: 'Description for issue1' - And I fill in issue search with 'Rock and roll' - Then I should not see 'Bugfix1' in issues - And I should not see "Release 0.4" in issues - And I should not see "Release 0.3" in issues - And I should not see "Tweet control" in issues - - # Markdown Scenario: Headers inside the description should have ids generated for them. From 78dd92b730063371742b6487ae2526d6cc0943b1 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 11:20:05 -0600 Subject: [PATCH 146/185] Improve styling of hover states --- app/assets/stylesheets/framework/filters.scss | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e47511940a7..8b7cb245420 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -89,16 +89,22 @@ overflow-y: hidden; border-radius: 0; + .dropdown-label-box { + border-color: $white-light; + border-style: solid; + border-width: 1px; + width: 17px; + height: 17px; + } + &:hover, &:focus { background-color: $dropdown-hover-color; color: $white-light; text-decoration: none; - .dropdown-label-box { + .avatar { border-color: $white-light; - border-style: solid; - border-width: 2px; } } } From 89204c22f15720080f71e424e50bad9485a49172 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 12:19:03 -0600 Subject: [PATCH 147/185] Add specs for addWordToInput --- ...ltered_search_dropdown_manager_spec.js.es6 | 76 ++++++++++++++----- 1 file changed, 55 insertions(+), 21 deletions(-) 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 4a358bd43e3..17d414aaad1 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 @@ -4,51 +4,85 @@ (() => { describe('Filtered Search Dropdown Manager', () => { describe('addWordToInput', () => { - describe('add word and when lastToken is an empty object', () => { - function getInput() { - return document.querySelector('.filtered-search'); - } + function getInputValue() { + return document.querySelector('.filtered-search').value; + } + beforeEach(() => { + const input = document.createElement('input'); + input.classList.add('filtered-search'); + document.body.appendChild(input); + + expect(input.value).toBe(''); + }); + + afterEach(() => { + document.querySelector('.filtered-search').outerHTML = ''; + }); + + describe('input has no existing value', () => { beforeEach(() => { spyOn(gl.FilteredSearchTokenizer, 'processTokens') .and.callFake(() => ({ lastToken: {}, })); - - const input = document.createElement('input'); - input.classList.add('filtered-search'); - document.body.appendChild(input); - - expect(input.value).toBe(''); - }); - - afterEach(() => { - document.querySelector('.filtered-search').outerHTML = ''; }); it('should add word', () => { gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInput().value).toBe('firstWord'); + expect(getInputValue()).toBe('firstWord'); }); it('should not add space before first word', () => { gl.FilteredSearchDropdownManager.addWordToInput('firstWord', true); - expect(getInput().value).toBe('firstWord'); + expect(getInputValue()).toBe('firstWord'); }); it('should not add space before second word by default', () => { gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInput().value).toBe('firstWord'); + expect(getInputValue()).toBe('firstWord'); gl.FilteredSearchDropdownManager.addWordToInput('secondWord'); - expect(getInput().value).toBe('firstWordsecondWord'); + expect(getInputValue()).toBe('firstWordsecondWord'); }); it('should add space before new word when addSpace is passed', () => { - expect(getInput().value).toBe(''); + expect(getInputValue()).toBe(''); gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInput().value).toBe('firstWord'); + expect(getInputValue()).toBe('firstWord'); gl.FilteredSearchDropdownManager.addWordToInput('secondWord', true); - expect(getInput().value).toBe('firstWord secondWord'); + expect(getInputValue()).toBe('firstWord secondWord'); + }); + }); + + describe('input has exsting value', () => { + it('should only add the remaining characters of the word', () => { + const lastToken = { + key: 'author', + value: 'roo', + }; + + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.callFake(() => ({ + lastToken, + })); + + document.querySelector('.filtered-search').value = `${lastToken.key}:${lastToken.value}`; + gl.FilteredSearchDropdownManager.addWordToInput('root'); + expect(getInputValue()).toBe('author:root'); + }); + + it('should only add the remaining characters of the word (contains space)', () => { + const lastToken = { + key: 'label', + value: 'test me', + }; + + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.callFake(() => ({ + lastToken, + })); + + document.querySelector('.filtered-search').value = `${lastToken.key}:"${lastToken.value}"`; + gl.FilteredSearchDropdownManager.addWordToInput('~\'"test me"\''); + expect(getInputValue()).toBe('label:~\'"test me"\''); }); }); }); From 776f1aaae4a8125f6f46ed4a095566a00ea2aa45 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 12:54:24 -0600 Subject: [PATCH 148/185] Add specs for filtered search token keys --- .../filtered_search_token_keys.js.es6 | 4 + .../filtered_search_token_keys_spec.js.es6 | 104 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 index a1830d13e5f..6bd9cb06362 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -44,6 +44,10 @@ return tokenKeys; } + static getConditions() { + return conditions; + } + static searchByKey(key) { return tokenKeys.find(tokenKey => tokenKey.key === key) || null; } diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 new file mode 100644 index 00000000000..6df7c0e44ef --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 @@ -0,0 +1,104 @@ +//= require extensions/array +//= require filtered_search/filtered_search_token_keys + +(() => { + describe('Filtered Search Token Keys', () => { + describe('get', () => { + let tokenKeys; + + beforeEach(() => { + tokenKeys = gl.FilteredSearchTokenKeys.get(); + }); + + it('should return tokenKeys', () => { + expect(tokenKeys !== null).toBe(true); + }); + + it('should return tokenKeys as an array', () => { + expect(tokenKeys instanceof Array).toBe(true); + }); + }); + + describe('getConditions', () => { + let conditions; + + beforeEach(() => { + conditions = gl.FilteredSearchTokenKeys.getConditions(); + }); + + it('should return conditions', () => { + expect(conditions !== null).toBe(true); + }); + + it('should return conditions as an array', () => { + expect(conditions instanceof Array).toBe(true); + }); + }); + + describe('searchByKey', () => { + it('should return null when key not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchBySymbol', () => { + it('should return null when symbol not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by symbol', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByKeyParam', () => { + it('should return null when key param not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key param', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByConditionUrl', () => { + it('should return null when condition url not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by url', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + expect(result).toBe(conditions[0]); + }); + }); + + describe('searchByConditionKeyValue', () => { + it('should return null when condition tokenKey and value not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by tokenKey and value', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); + expect(result).toEqual(conditions[0]); + }); + }); + }); +})(); From c94b64813399633ccb3a20c0f09c2e70aa7bbc0c Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 14:16:29 -0600 Subject: [PATCH 149/185] Add specs to filtered search tokenizer --- .../filtered_search_tokenizer_spec.js.es6 | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 new file mode 100644 index 00000000000..c93f163e763 --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -0,0 +1,271 @@ +//= require extensions/array +//= require filtered_search/filtered_search_token_keys +//= require filtered_search/filtered_search_tokenizer + +(() => { + describe('Filtered Search Tokenizer', () => { + describe('parseToken', () => { + it('should return key, value and symbol', () => { + const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer + .parseToken('author:@user'); + + expect(tokenKey).toBe('author'); + expect(tokenValue).toBe('@user'); + expect(tokenSymbol).toBe('@'); + }); + + it('should return value with spaces', () => { + const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer + .parseToken('label:~"test me"'); + + expect(tokenKey).toBe('label'); + expect(tokenValue).toBe('~"test me"'); + expect(tokenSymbol).toBe('~'); + }); + }); + + describe('getLastTokenObject', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchTokenizer, 'getLastToken').and.callFake(input => input); + }); + + it('should return key and value', () => { + const { key, value } = gl.FilteredSearchTokenizer.getLastTokenObject('author:@root'); + expect(key).toBe('author'); + expect(value).toBe(':@root'); + }); + + describe('string without colon', () => { + let lastTokenObject; + + beforeEach(() => { + lastTokenObject = gl.FilteredSearchTokenizer.getLastTokenObject('author'); + }); + + it('should return key as an empty string', () => { + expect(lastTokenObject.key).toBe(''); + }); + + it('should return input as value', () => { + expect(lastTokenObject.value).toBe('author'); + }); + }); + }); + + describe('getLastToken', () => { + it('returns entire string when there is only one word', () => { + const lastToken = gl.FilteredSearchTokenizer.getLastToken('input'); + expect(lastToken).toBe('input'); + }); + + it('returns last word when there are multiple words', () => { + const lastToken = gl.FilteredSearchTokenizer.getLastToken('this is a few words'); + expect(lastToken).toBe('words'); + }); + + it('returns last token when there are multiple tokens', () => { + const lastToken = gl.FilteredSearchTokenizer + .getLastToken('label:fun author:root milestone:2.0'); + expect(lastToken).toBe('milestone:2.0'); + }); + + it('returns last token containing spaces escaped by double quotes', () => { + const lastToken = gl.FilteredSearchTokenizer + .getLastToken('label:fun author:root milestone:2.0 label:~"Feature Proposal"'); + expect(lastToken).toBe('label:~"Feature Proposal"'); + }); + + it('returns last token containing spaces escaped by single quotes', () => { + const lastToken = gl.FilteredSearchTokenizer + .getLastToken('label:fun author:root milestone:2.0 label:~\'Feature Proposal\''); + expect(lastToken).toBe('label:~\'Feature Proposal\''); + }); + + it('returns last token containing special characters', () => { + const lastToken = gl.FilteredSearchTokenizer + .getLastToken('label:fun author:root milestone:2.0 label:~!@#$%^&*()'); + expect(lastToken).toBe('label:~!@#$%^&*()'); + }); + }); + + describe('processTokens', () => { + describe('input does not contain any tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + }); + + it('returns input as searchToken', () => { + expect(results.searchToken).toBe('searchTerm'); + }); + + it('returns tokens as an empty array', () => { + expect(results.tokens.length).toBe(0); + }); + + it('returns lastToken equal to searchToken', () => { + expect(results.lastToken).toBe(results.searchToken); + }); + }); + + describe('input contains only tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + }); + + it('returns searchToken as an empty string', () => { + expect(results.searchToken).toBe(''); + }); + + it('returns tokens array of size equal to the number of tokens in input', () => { + expect(results.tokens.length).toBe(4); + }); + + it('returns tokens array that matches the tokens found in input', () => { + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('@root'); + expect(results.tokens[0].wildcard).toBe(false); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('~Very Important'); + expect(results.tokens[1].wildcard).toBe(false); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('%v1.0'); + expect(results.tokens[2].wildcard).toBe(false); + + expect(results.tokens[3].key).toBe('assignee'); + expect(results.tokens[3].value).toBe('none'); + expect(results.tokens[3].wildcard).toBe(true); + }); + + it('returns lastToken equal to the last object in the tokens array', () => { + expect(results.tokens[3]).toBe(results.lastToken); + }); + }); + + describe('input starts with search value and ends with tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('searchTerm anotherSearchTerm milestone:none'); + }); + + it('returns searchToken', () => { + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + }); + + it('returns correct number of tokens', () => { + expect(results.tokens.length).toBe(1); + }); + + it('returns correct tokens', () => { + expect(results.tokens[0].key).toBe('milestone'); + expect(results.tokens[0].value).toBe('none'); + expect(results.tokens[0].wildcard).toBe(true); + }); + + it('returns lastToken', () => { + expect(results.tokens[0]).toBe(results.lastToken); + }); + }); + + describe('input starts with token and ends with search value', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('assignee:@user searchTerm'); + }); + + it('returns searchToken', () => { + expect(results.searchToken).toBe('searchTerm'); + }); + + it('returns correct number of tokens', () => { + expect(results.tokens.length).toBe(1); + }); + + it('returns correct tokens', () => { + expect(results.tokens[0].key).toBe('assignee'); + expect(results.tokens[0].value).toBe('@user'); + expect(results.tokens[0].wildcard).toBe(false); + }); + + it('returns lastToken as the searchTerm', () => { + expect(results.lastToken).toBe(results.searchToken); + }); + }); + + describe('input contains search value wrapped between tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); + }); + + it('returns searchToken', () => { + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + }); + + it('returns correct number of tokens', () => { + expect(results.tokens.length).toBe(3); + }); + + + it('returns tokens array in the order it was processed', () => { + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('@root'); + expect(results.tokens[0].wildcard).toBe(false); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('~Won\'t fix'); + expect(results.tokens[1].wildcard).toBe(false); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('none'); + expect(results.tokens[2].wildcard).toBe(true); + }); + + it('returns lastToken', () => { + expect(results.tokens[2]).toBe(results.lastToken); + }); + }); + + describe('input search value is spaced in between tokens', () => { + let results; + beforeEach(() => { + results = gl.FilteredSearchTokenizer + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + }); + + it('returns searchToken', () => { + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + }); + + it('returns correct number of tokens', () => { + expect(results.tokens.length).toBe(3); + }); + + it('returns tokens array in the order it was processed', () => { + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('@root'); + expect(results.tokens[0].wildcard).toBe(false); + + expect(results.tokens[1].key).toBe('assignee'); + expect(results.tokens[1].value).toBe('none'); + expect(results.tokens[1].wildcard).toBe(true); + + expect(results.tokens[2].key).toBe('label'); + expect(results.tokens[2].value).toBe('~Doing'); + expect(results.tokens[2].wildcard).toBe(false); + }); + + it('returns lastToken', () => { + expect(results.tokens[2]).toBe(results.lastToken); + }); + }); + }); + }); +})(); From 43850df63e342f912d8a8b0989364be6b26e655f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 15:10:45 -0600 Subject: [PATCH 150/185] Add user symbol for search spec --- spec/features/search_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 9a7079848a5..a05b83959fb 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -170,7 +170,7 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.filtered-search') - expect(find('.filtered-search').value).to eq("assignee:#{user.username}") + expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") end it 'takes user to her issues page when issues authored is clicked' do @@ -178,7 +178,7 @@ describe "Search", feature: true do sleep 2 expect(page).to have_selector('.filtered-search') - expect(find('.filtered-search').value).to eq("author:#{user.username}") + expect(find('.filtered-search').value).to eq("author:@#{user.username}") end it 'takes user to her MR page when MR assigned is clicked' do From cc34439d56b35f2175c979ed85b655280bbdd899 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 15:57:35 -0600 Subject: [PATCH 151/185] Fix RSS feed test --- spec/features/issues/filter_issues_spec.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index cbb11b790ec..391c8905630 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -3,8 +3,8 @@ require 'rails_helper' describe 'Filter issues', feature: true do include WaitForAjax - let!(:project) { create(:project) } let!(:group) { create(:group) } + let!(:project) { create(:project, group: group) } let!(:user) { create(:user) } let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } @@ -652,30 +652,30 @@ describe 'Filter issues', feature: true do describe 'RSS feeds' do it 'updates atom feed link for project issues' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) + visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') params = CGI::parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) + expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) end it 'updates atom feed link for group issues' do - visit issues_group_path(group, milestone_title: '', assignee_id: user.id) + visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') params = CGI::parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) + expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) end end From 4f774c940f1bbcadaead168e6ee5dd5c54864c7f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:11:37 -0600 Subject: [PATCH 152/185] Remove if issue.boards since search bar does not display on issue boards page --- app/views/shared/issuable/_search_bar.html.haml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index aca39941381..896769768eb 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -82,20 +82,7 @@ %span.label-title.js-data-value {{title}} .pull-right - - if boards_page - #js-boards-seach.issue-boards-search - %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - - if can?(current_user, :admin_list, @project) - .dropdown.pull-right - %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } - Create new list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" } - - if can?(current_user, :admin_label, @project) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading - - else - = render 'shared/sort_dropdown' + = render 'shared/sort_dropdown' - if @bulk_edit .issues_bulk_update.hide From 206d442b7a11312a759530b139bb481f4dc48718 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:11:45 -0600 Subject: [PATCH 153/185] Refine search bar specs --- spec/features/issues/search_bar_spec.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/features/issues/search_bar_spec.rb b/spec/features/issues/search_bar_spec.rb index 1d632671fe2..d0abdc284ea 100644 --- a/spec/features/issues/search_bar_spec.rb +++ b/spec/features/issues/search_bar_spec.rb @@ -1,22 +1,20 @@ require 'rails_helper' -describe 'Search bar', feature: true do +describe 'Search bar', js: true, feature: true do include WaitForAjax - let!(:project) { create(:project) } - let!(:group) { create(:group) } - let!(:user) { create(:user) } + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } before do project.team << [user, :master] - group.add_developer(user) login_as(user) create(:issue, project: project) visit namespace_project_issues_path(project.namespace, project) end - describe 'clear search button', js: true do + describe 'clear search button' do it 'clears text' do search_text = 'search_text' filtered_search = find('.filtered-search') From 63ff055eccefd97cb7014aab94a624f338057dd6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 16:18:16 -0600 Subject: [PATCH 154/185] Change CGI::Parse to CGI.Parse --- spec/features/issues/filter_issues_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 391c8905630..8911b919cf7 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -654,9 +654,9 @@ describe 'Filter issues', feature: true do it 'updates atom feed link for project issues' do visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) + params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) @@ -668,9 +668,9 @@ describe 'Filter issues', feature: true do it 'updates atom feed link for group issues' do visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) + params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) From 98189f37c94a6c97d7420e66deb25a000ee65afc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 18:53:51 -0600 Subject: [PATCH 155/185] Remove trailing whitespace --- spec/features/issues/filter_issues_spec.rb | 40 +++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 8911b919cf7..0b94bcc4e3f 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -67,19 +67,19 @@ describe 'Filter issues', feature: true do assignee: user) issue.labels << bug_label - issue_with_caps_label = create(:issue, - title: "issue by assignee with searchTerm and label", - project: project, - milestone: milestone, - author: user, + issue_with_caps_label = create(:issue, + title: "issue by assignee with searchTerm and label", + project: project, + milestone: milestone, + author: user, assignee: user) issue_with_caps_label.labels << caps_sensitive_label - issue_with_everything = create(:issue, - title: "Bug report with everything you thought was possible", - project: project, - milestone: milestone, - author: user, + issue_with_everything = create(:issue, + title: "Bug report with everything you thought was possible", + project: project, + milestone: milestone, + author: user, assignee: user) issue_with_everything.labels << bug_label issue_with_everything.labels << caps_sensitive_label @@ -590,17 +590,17 @@ describe 'Filter issues', feature: true do context 'sorting', js: true do it 'sorts by oldest updated' do - create(:issue, - title: '3 days ago', - project: project, - author: user, + create(:issue, + title: '3 days ago', + project: project, + author: user, created_at: 3.days.ago, updated_at: 3.days.ago) - old_issue = create(:issue, - title: '5 days ago', - project: project, - author: user, + old_issue = create(:issue, + title: '5 days ago', + project: project, + author: user, created_at: 5.days.ago, updated_at: 5.days.ago) @@ -609,10 +609,10 @@ describe 'Filter issues', feature: true do sort_toggle = find('.filtered-search-container .dropdown-toggle') sort_toggle.click - + find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click wait_for_ajax - + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) end end From d19303cbe1e9813f5fe2409908c7f89616ec5eac Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 23:08:49 -0600 Subject: [PATCH 156/185] Fix dropdown hint reset when changing tabs --- .../javascripts/filtered_search/dropdown_hint.js.es6 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 34079b25846..b9f552b62b9 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -59,7 +59,15 @@ renderContent() { this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); - this.droplab.setData(this.hookId, dropdownData); + + // Clone dropdownData to prevent it from being + // changed due to pass by reference + const data = []; + dropdownData.forEach((item) => { + data.push(Object.assign({}, item)); + }); + + this.droplab.setData(this.hookId, data); } init() { From 61680a2d9833cc3ef63b9e76930f47e44258f30d Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 15 Dec 2016 23:20:31 -0600 Subject: [PATCH 157/185] Add selected tagName check for itemClicked --- .../filtered_search/dropdown_hint.js.es6 | 20 ++++++++++--------- .../filtered_search_dropdown.js.es6 | 15 ++++++++------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index b9f552b62b9..bdcece61984 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -35,18 +35,20 @@ itemClicked(e) { const { selected } = e.detail; - if (selected.hasAttribute('data-value')) { + if (selected.tagName === 'LI') { + if (selected.hasAttribute('data-value')) { this.dismissDropdown(); - } else { - const token = selected.querySelector('.js-filter-hint').innerText.trim(); - const tag = selected.querySelector('.js-filter-tag').innerText.trim(); + } else { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); - if (tag.length) { - gl.FilteredSearchDropdownManager - .addWordToInput(this.getSelectedTextWithoutEscaping(token)); + if (tag.length) { + gl.FilteredSearchDropdownManager + .addWordToInput(this.getSelectedTextWithoutEscaping(token)); + } + this.dismissDropdown(); + this.dispatchInputEvent(); } - this.dismissDropdown(); - this.dispatchInputEvent(); } } 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 6c66a3b0613..68014e27462 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -28,14 +28,17 @@ itemClicked(e, getValueFunction) { const { selected } = e.detail; - const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); - if (!dataValueSet) { - const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(value); + if (selected.tagName === 'LI') { + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); + + if (!dataValueSet) { + const value = getValueFunction(selected); + gl.FilteredSearchDropdownManager.addWordToInput(value); + } + + this.dismissDropdown(); } - - this.dismissDropdown(); } setAsDropdown() { From 625ecdbcc848f05eafaf167255901efd2d32cb37 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 01:05:27 -0600 Subject: [PATCH 158/185] Create dropdown hint spec --- .../filtered_search/dropdown_hint_spec.rb | 113 ++++++++++++++++++ .../filter_issues_spec.rb | 12 +- .../{ => filtered_search}/search_bar_spec.rb | 33 +++++ 3 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 spec/features/issues/filtered_search/dropdown_hint_spec.rb rename spec/features/issues/{ => filtered_search}/filter_issues_spec.rb (98%) rename spec/features/issues/{ => filtered_search}/search_bar_spec.rb (52%) diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb new file mode 100644 index 00000000000..364d4bf4db1 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -0,0 +1,113 @@ +require 'rails_helper' + +describe 'Dropdown hint', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let(:filtered_search) { find('.filtered-search') } + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + before do + expect(page).to have_css('#js-dropdown-hint', visible: false) + filtered_search.click(); + end + + it 'opens when the search bar is first focused' do + expect(page).to have_css('#js-dropdown-hint', visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click(); + expect(page).to have_css('#js-dropdown-hint', visible: false) + end + end + + describe 'filtering' do + it 'does not filter `Keep typing and press Enter`' do + filtered_search.set('randomtext') + expect(page).to have_css('#js-dropdown-hint', text: 'Keep typing and press Enter', visible: false) + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) + end + + it 'filters with text' do + filtered_search.set('a') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(3) + end + end + + describe 'selecting from dropdown with no input' do + before do + filtered_search.click + end + + it 'opens the author dropdown when you click on author' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-author', visible: true) + expect(filtered_search.value).to eq('author:') + end + + it 'opens the assignee dropdown when you click on assignee' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'assignee').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-assignee', visible: true) + expect(filtered_search.value).to eq('assignee:') + end + + it 'opens the milestone dropdown when you click on milestone' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'milestone').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-milestone', visible: true) + expect(filtered_search.value).to eq('milestone:') + end + + it 'opens the label dropdown when you click on label' do + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'label').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-label', visible: true) + expect(filtered_search.value).to eq('label:') + end + end + + describe 'selecting from dropdown with some input' do + it 'opens the author dropdown when you click on author' do + filtered_search.set('auth') + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-author', visible: true) + expect(filtered_search.value).to eq('author:') + end + + it 'opens the assignee dropdown when you click on assignee' do + filtered_search.set('assign') + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'assignee').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-assignee', visible: true) + expect(filtered_search.value).to eq('assignee:') + end + + it 'opens the milestone dropdown when you click on milestone' do + filtered_search.set('mile') + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'milestone').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-milestone', visible: true) + expect(filtered_search.value).to eq('milestone:') + end + + it 'opens the label dropdown when you click on label' do + filtered_search.set('lab') + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'label').click + expect(page).to have_css('#js-dropdown-hint', visible: false) + expect(page).to have_css('#js-dropdown-label', visible: true) + expect(filtered_search.value).to eq('label:') + end + end +end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb similarity index 98% rename from spec/features/issues/filter_issues_spec.rb rename to spec/features/issues/filtered_search/filter_issues_spec.rb index 0b94bcc4e3f..283814d2cbb 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -3,13 +3,13 @@ require 'rails_helper' describe 'Filter issues', feature: true do include WaitForAjax - let!(:group) { create(:group) } - let!(:project) { create(:project, group: group) } - let!(:user) { create(:user) } - let!(:user2) { create(:user) } + let!(:group) { create(:group) } + let!(:project) { create(:project, group: group) } + let!(:user) { create(:user) } + let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } - let!(:label) { create(:label, project: project) } - let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + let!(:label) { create(:label, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:bug_label) { create(:label, project: project, title: 'bug') } let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } diff --git a/spec/features/issues/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb similarity index 52% rename from spec/features/issues/search_bar_spec.rb rename to spec/features/issues/filtered_search/search_bar_spec.rb index d0abdc284ea..5862214cdc3 100644 --- a/spec/features/issues/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -14,6 +14,11 @@ describe 'Search bar', js: true, feature: true do visit namespace_project_issues_path(project.namespace, project) end + def getLeftStyle(style) + leftStyle = /left:\s\d*[.]\d*px/.match(style) + leftStyle.to_s.gsub('left: ', '').to_f; + end + describe 'clear search button' do it 'clears text' do search_text = 'search_text' @@ -49,5 +54,33 @@ describe 'Search bar', js: true, feature: true do expect(page).to have_css('.clear-search', visible: true) end + + it 'resets the dropdown hint filter' do + filtered_search = find('.filtered-search') + filtered_search.click(); + original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size + + filtered_search.set('author') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1) + + find('.filtered-search-input-container .clear-search').click + filtered_search.click() + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size) + end + + it 'resets the dropdown filters' do + filtered_search = find('.filtered-search') + filtered_search.set('a') + hintStyle = page.find('#js-dropdown-hint')['style'] + hintOffset = getLeftStyle(hintStyle) + + filtered_search.set('author:') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) + + find('.filtered-search-input-container .clear-search').click + filtered_search.click() + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 + expect(getLeftStyle(page.find('#js-dropdown-hint')['style'])).to eq (hintOffset) + end end end From fd90a99639f5685aa86440aba1da6165b623e877 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 01:08:07 -0600 Subject: [PATCH 159/185] Define filtered_search as a variable --- spec/features/issues/filtered_search/dropdown_hint_spec.rb | 1 + spec/features/issues/filtered_search/search_bar_spec.rb | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 364d4bf4db1..216cd78850b 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -6,6 +6,7 @@ describe 'Dropdown hint', js: true, feature: true do let!(:project) { create(:empty_project) } let!(:user) { create(:user) } let(:filtered_search) { find('.filtered-search') } + before do project.team << [user, :master] login_as(user) diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 5862214cdc3..d37057a44f8 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -5,6 +5,7 @@ describe 'Search bar', js: true, feature: true do let!(:project) { create(:empty_project) } let!(:user) { create(:user) } + let(:filtered_search) { find('.filtered-search') } before do project.team << [user, :master] @@ -22,7 +23,6 @@ describe 'Search bar', js: true, feature: true do describe 'clear search button' do it 'clears text' do search_text = 'search_text' - filtered_search = find('.filtered-search') filtered_search.set(search_text) expect(filtered_search.value).to eq(search_text) @@ -35,28 +35,24 @@ describe 'Search bar', js: true, feature: true do end it 'hides after clicked' do - filtered_search = find('.filtered-search') filtered_search.set('a') find('.filtered-search-input-container .clear-search').click expect(page).to have_css('.clear-search', visible: false) end it 'hides when there is no text' do - filtered_search = find('.filtered-search') filtered_search.set('a') filtered_search.set('') expect(page).to have_css('.clear-search', visible: false) end it 'shows when there is text' do - filtered_search = find('.filtered-search') filtered_search.set('a') expect(page).to have_css('.clear-search', visible: true) end it 'resets the dropdown hint filter' do - filtered_search = find('.filtered-search') filtered_search.click(); original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size @@ -69,7 +65,6 @@ describe 'Search bar', js: true, feature: true do end it 'resets the dropdown filters' do - filtered_search = find('.filtered-search') filtered_search.set('a') hintStyle = page.find('#js-dropdown-hint')['style'] hintOffset = getLeftStyle(hintStyle) From 9c4868141273b536cc0bc7fb80a662789fe89286 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 10:22:09 -0600 Subject: [PATCH 160/185] Make changes to make it more flexible for new filters --- .../javascripts/filtered_search/dropdown_hint.js.es6 | 2 +- .../filtered_search/filtered_search_manager.js.es6 | 12 +++++++++--- .../filtered_search_token_keys.js.es6 | 10 +++++++++- app/assets/stylesheets/framework/filters.scss | 4 ++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index bdcece61984..7bf30143d78 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -37,7 +37,7 @@ if (selected.tagName === 'LI') { if (selected.hasAttribute('data-value')) { - this.dismissDropdown(); + this.dismissDropdown(); } else { const token = selected.querySelector('.js-filter-hint').innerText.trim(); const tag = selected.querySelector('.js-filter-tag').innerText.trim(); 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 e5b37f1e691..565f2347072 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -101,7 +101,8 @@ const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); if (match) { - const sanitizedKey = keyParam.slice(0, keyParam.indexOf('_')); + const indexOf = keyParam.indexOf('_'); + const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; const symbol = match.symbol; let quotationsToUse = ''; @@ -137,14 +138,19 @@ const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); let tokenPath = ''; + let keyParam = token.key; + if (param) { + keyParam += `_${param}`; + } + if (token.wildcard && condition) { tokenPath = condition.url; } else if (token.wildcard) { // wildcard means that the token does not have a symbol - tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value)}`; + tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`; } else { // Remove the token symbol - tokenPath = `${token.key}_${param}=${encodeURIComponent(token.value.slice(1))}`; + tokenPath = `${keyParam}=${encodeURIComponent(token.value.slice(1))}`; } paths.push(tokenPath); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 index 6bd9cb06362..e46373024b6 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -57,7 +57,15 @@ } static searchByKeyParam(keyParam) { - return tokenKeys.find(tokenKey => keyParam === `${tokenKey.key}_${tokenKey.param}`) || null; + return tokenKeys.find((tokenKey) => { + let tokenKeyParam = tokenKey.key; + + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } + + return keyParam === tokenKeyParam; + }) || null; } static searchByConditionUrl(url) { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 8b7cb245420..fee38b05023 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -89,6 +89,10 @@ overflow-y: hidden; border-radius: 0; + .fa { + width: 15px; + } + .dropdown-label-box { border-color: $white-light; border-style: solid; From e05d6b1cfac01c713016f20efae6962c3249e951 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 16:25:52 -0600 Subject: [PATCH 161/185] Add specs for remaining dropdowns --- .../filtered_search/dropdown_assignee_spec.rb | 148 ++++++++++++ .../filtered_search/dropdown_author_spec.rb | 141 +++++++++++ .../filtered_search/dropdown_hint_spec.rb | 57 +++-- .../filtered_search/dropdown_label_spec.rb | 211 +++++++++++++++++ .../dropdown_milestone_spec.rb | 222 ++++++++++++++++++ .../filtered_search/filter_issues_spec.rb | 40 ++-- .../issues/filtered_search/search_bar_spec.rb | 14 +- 7 files changed, 782 insertions(+), 51 deletions(-) create mode 100644 spec/features/issues/filtered_search/dropdown_assignee_spec.rb create mode 100644 spec/features/issues/filtered_search/dropdown_author_spec.rb create mode 100644 spec/features/issues/filtered_search/dropdown_label_spec.rb create mode 100644 spec/features/issues/filtered_search/dropdown_milestone_spec.rb diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb new file mode 100644 index 00000000000..5d0e95580c9 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -0,0 +1,148 @@ +require 'rails_helper' + +describe 'Dropdown assignee', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_assignee) { '#js-dropdown-assignee' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + end + end + + def dropdown_assignee_size + page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size + end + + def click_assignee(text) + find('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + project.team << [user_john, :master] + project.team << [user_jacob, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has assignee:' do + filtered_search.set('assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + expect(page).to have_css(js_dropdown_assignee, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('assignee:') + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('assignee:') + expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading') + end + + it 'should load all the assignees when opened' do + send_keys_to_filtered_search('assignee:') + expect(dropdown_assignee_size).to eq(3) + end + end + + describe 'filtering' do + before do + filtered_search.set('assignee:') + end + + it 'filters by name' do + send_keys_to_filtered_search('j') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('J') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by username with symbol' do + send_keys_to_filtered_search('@ot') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive username with symbol' do + send_keys_to_filtered_search('@OT') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by username without symbol' do + send_keys_to_filtered_search('ot') + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive username without symbol' do + send_keys_to_filtered_search('OT') + expect(dropdown_assignee_size).to eq(2) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('assignee:') + end + + it 'fills in the assignee username when the assignee has not been filtered' do + click_assignee(user_jacob.name) + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") + end + + it 'fills in the assignee username when the assignee has been filtered' do + send_keys_to_filtered_search('roo') + click_assignee(user.name) + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:@#{user.username}") + end + + it 'selects `no assignee`' do + click_assignee('No Assignee') + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:none") + end + end + + describe 'input has existing content' do + it 'opens assignee dropdown with existing search term' do + filtered_search.set('searchTerm assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing author' do + filtered_search.set('author:@user assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing label' do + filtered_search.set('label:~bug assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing milestone' do + filtered_search.set('milestone:%v1.0 assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb new file mode 100644 index 00000000000..c7cb5e25174 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -0,0 +1,141 @@ +require 'rails_helper' + +describe 'Dropdown author', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_author) { '#js-dropdown-author' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + end + end + + def dropdown_author_size + page.all('#js-dropdown-author .filter-dropdown .filter-dropdown-item').size + end + + def click_author(text) + find('#js-dropdown-author .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + project.team << [user_john, :master] + project.team << [user_jacob, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has author:' do + filtered_search.set('author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + expect(page).to have_css(js_dropdown_author, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('author:') + expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('author:') + expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading') + end + + it 'should load all the authors when opened' do + send_keys_to_filtered_search('author:') + expect(dropdown_author_size).to eq(3) + end + end + + describe 'filtering' do + before do + filtered_search.set('author:') + end + + it 'filters by name' do + send_keys_to_filtered_search('j') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('J') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by username with symbol' do + send_keys_to_filtered_search('@ot') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by case insensitive username with symbol' do + send_keys_to_filtered_search('@OT') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by username without symbol' do + send_keys_to_filtered_search('ot') + expect(dropdown_author_size).to eq(2) + end + + it 'filters by case insensitive username without symbol' do + send_keys_to_filtered_search('OT') + expect(dropdown_author_size).to eq(2) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('author:') + end + + it 'fills in the author username when the author has not been filtered' do + click_author(user_jacob.name) + expect(page).to have_css(js_dropdown_author, visible: false) + expect(filtered_search.value).to eq("author:@#{user_jacob.username}") + end + + it 'fills in the author username when the author has been filtered' do + click_author(user.name) + expect(page).to have_css(js_dropdown_author, visible: false) + expect(filtered_search.value).to eq("author:@#{user.username}") + end + end + + describe 'input has existing content' do + it 'opens author dropdown with existing search term' do + filtered_search.set('searchTerm author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing assignee' do + filtered_search.set('assignee:@user author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing label' do + filtered_search.set('label:~bug author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing milestone' do + filtered_search.set('milestone:%v1.0 author:') + expect(page).to have_css(js_dropdown_author, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 216cd78850b..c4328bc18b0 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -6,6 +6,15 @@ describe 'Dropdown hint', js: true, feature: true do let!(:project) { create(:empty_project) } let!(:user) { create(:user) } let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_hint) { '#js-dropdown-hint' } + + def dropdown_hint_size + page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size + end + + def click_hint(text) + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click + end before do project.team << [user, :master] @@ -17,30 +26,30 @@ describe 'Dropdown hint', js: true, feature: true do describe 'behavior' do before do - expect(page).to have_css('#js-dropdown-hint', visible: false) - filtered_search.click(); + expect(page).to have_css(js_dropdown_hint, visible: false) + filtered_search.click() end it 'opens when the search bar is first focused' do - expect(page).to have_css('#js-dropdown-hint', visible: true) + expect(page).to have_css(js_dropdown_hint, visible: true) end it 'closes when the search bar is unfocused' do - find('body').click(); - expect(page).to have_css('#js-dropdown-hint', visible: false) + find('body').click() + expect(page).to have_css(js_dropdown_hint, visible: false) end end describe 'filtering' do it 'does not filter `Keep typing and press Enter`' do filtered_search.set('randomtext') - expect(page).to have_css('#js-dropdown-hint', text: 'Keep typing and press Enter', visible: false) - expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) + expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false) + expect(dropdown_hint_size).to eq(0) end it 'filters with text' do filtered_search.set('a') - expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(3) + expect(dropdown_hint_size).to eq(3) end end @@ -50,29 +59,29 @@ describe 'Dropdown hint', js: true, feature: true do end it 'opens the author dropdown when you click on author' do - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('author') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) expect(filtered_search.value).to eq('author:') end it 'opens the assignee dropdown when you click on assignee' do - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'assignee').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('assignee') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(filtered_search.value).to eq('assignee:') end it 'opens the milestone dropdown when you click on milestone' do - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'milestone').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('milestone') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(filtered_search.value).to eq('milestone:') end it 'opens the label dropdown when you click on label' do - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'label').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('label') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) expect(filtered_search.value).to eq('label:') end @@ -81,32 +90,32 @@ describe 'Dropdown hint', js: true, feature: true do describe 'selecting from dropdown with some input' do it 'opens the author dropdown when you click on author' do filtered_search.set('auth') - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('author') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) expect(filtered_search.value).to eq('author:') end it 'opens the assignee dropdown when you click on assignee' do filtered_search.set('assign') - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'assignee').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('assignee') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(filtered_search.value).to eq('assignee:') end it 'opens the milestone dropdown when you click on milestone' do filtered_search.set('mile') - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'milestone').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('milestone') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(filtered_search.value).to eq('milestone:') end it 'opens the label dropdown when you click on label' do filtered_search.set('lab') - find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'label').click - expect(page).to have_css('#js-dropdown-hint', visible: false) + click_hint('label') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) expect(filtered_search.value).to eq('label:') end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb new file mode 100644 index 00000000000..d007e160b3e --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -0,0 +1,211 @@ +require 'rails_helper' + +describe 'Dropdown label', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:uppercase_label) { create(:label, project: project, title: 'BUG') } + let!(:two_words_label) { create(:label, project: project, title: 'High Priority') } + let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') } + let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')} + let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')} + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_label) { '#js-dropdown-label' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + sleep 3 + end + end + + def dropdown_label_size + page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size + end + + def click_label(text) + find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has label:' do + filtered_search.set('label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + expect(page).to have_css(js_dropdown_label, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('label:') + expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('label:') + expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading') + end + + it 'should load all the labels when opened' do + send_keys_to_filtered_search('label:') + expect(dropdown_label_size).to be > 0 + end + end + + describe 'filtering' do + before do + filtered_search.set('label:') + end + + it 'filters by name' do + send_keys_to_filtered_search('b') + expect(dropdown_label_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('B') + expect(dropdown_label_size).to eq(2) + end + + it 'filters by name with symbol' do + send_keys_to_filtered_search('~bu') + expect(dropdown_label_size).to eq(2) + end + + it 'filters by case insensitive name with symbol' do + send_keys_to_filtered_search('~BU') + expect(dropdown_label_size).to eq(2) + end + + it 'filters by multiple names using double quotes' do + send_keys_to_filtered_search('"High P') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple names using single quotes' do + send_keys_to_filtered_search('\'High P') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple names using single and double quotes' do + send_keys_to_filtered_search('~"won`\'t f') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple names using double quotes with symbol' do + send_keys_to_filtered_search('~"High P') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple names using single quotes with symbol' do + send_keys_to_filtered_search('~\'High P') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by special characters' do + send_keys_to_filtered_search('^+') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by special characters with symbol' do + send_keys_to_filtered_search('~^+') + expect(dropdown_label_size).to eq(1) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('label:') + end + + it 'fills in the label name when the label has not been filled' do + click_label(bug_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{bug_label.title}") + end + + it 'fills in the label name when the label is partially filled' do + send_keys_to_filtered_search('bu') + click_label(bug_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{bug_label.title}") + end + + it 'fills in the label name that contains multiple words' do + click_label(two_words_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") + end + + it 'fills in the label name that contains multiple words and is very long' do + click_label(long_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") + end + + it 'fills in the label name that contains double quotes' do + click_label(wont_fix_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") + end + + it 'fills in the label name with the correct capitalization' do + click_label(uppercase_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") + end + + it 'fills in the label name with special characters' do + click_label(special_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{special_label.title}") + end + + it 'selects `no label`' do + click_label('No Label') + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:none") + end + end + + describe 'input has existing content' do + it 'opens label dropdown with existing search term' do + filtered_search.set('searchTerm label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing author' do + filtered_search.set('author:@person label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing assignee' do + filtered_search.set('assignee:@person label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing label' do + filtered_search.set('label:~urgent label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing milestone' do + filtered_search.set('milestone:%v2.0 label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb new file mode 100644 index 00000000000..d367430a670 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -0,0 +1,222 @@ +require 'rails_helper' + +describe 'Dropdown milestone', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let!(:milestone) { create(:milestone, title: 'v1.0', project: project) } + let!(:uppercase_milestone) { create(:milestone, title: 'CAP_MILESTONE', project: project) } + let!(:two_words_milestone) { create(:milestone, title: 'Future Plan', project: project) } + let!(:wont_fix_milestone) { create(:milestone, title: 'Won"t Fix', project: project) } + let!(:special_milestone) { create(:milestone, title: '!@#$%^&*(+)', project: project) } + let!(:long_milestone) { create(:milestone, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title', project: project) } + + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_milestone) { '#js-dropdown-milestone' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + sleep 3 + end + end + + def dropdown_milestone_size + page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size + end + + def click_milestone(text) + find('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', text: text).click + end + + def click_static_milestone(text) + find('#js-dropdown-milestone .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has milestone:' do + filtered_search.set('milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + expect(page).to have_css(js_dropdown_milestone, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('milestone:') + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('milestone:') + expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading') + end + + it 'should load all the milestones when opened' do + send_keys_to_filtered_search('milestone:') + expect(dropdown_milestone_size).to be > 0 + end + end + + describe 'filtering' do + before do + filtered_search.set('milestone:') + end + + it 'filters by name' do + send_keys_to_filtered_search('v1') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('V1') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by name with symbol' do + send_keys_to_filtered_search('%v1') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by case insensitive name with symbol' do + send_keys_to_filtered_search('%V1') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using double quotes' do + send_keys_to_filtered_search('"future') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using single quotes' do + send_keys_to_filtered_search('\'future p') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using single and double quotes' do + send_keys_to_filtered_search('%"won`\'t f') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using double quotes with symbol' do + send_keys_to_filtered_search('%"future p') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by multiple names using single quotes with symbol' do + send_keys_to_filtered_search('%\'future p') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by special characters' do + send_keys_to_filtered_search('^+') + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by special characters with symbol' do + send_keys_to_filtered_search('~^+') + expect(dropdown_milestone_size).to eq(1) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('milestone:') + end + + it 'fills in the milestone name when the milestone has not been filled' do + click_milestone(milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + end + + it 'fills in the milestone name when the milestone is partially filled' do + send_keys_to_filtered_search('v') + click_milestone(milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + end + + it 'fills in the milestone name that contains multiple words' do + click_milestone(two_words_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") + end + + it 'fills in the milestone name that contains multiple words and is very long' do + click_milestone(long_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") + end + + it 'fills in the milestone name that contains double quotes' do + click_milestone(wont_fix_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") + end + + it 'fills in the milestone name with the correct capitalization' do + click_milestone(uppercase_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") + end + + it 'fills in the milestone name with special characters' do + click_milestone(special_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") + end + + it 'selects `no milestone`' do + click_static_milestone('No Milestone') + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:none") + end + + it 'selects `upcoming milestone`' do + click_static_milestone('Upcoming') + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:upcoming") + end + end + + describe 'input has existing content' do + it 'opens milestone dropdown with existing search term' do + filtered_search.set('searchTerm milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing author' do + filtered_search.set('author:@john milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing assignee' do + filtered_search.set('assignee:@john milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing label' do + filtered_search.set('label:~important milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing milestone' do + filtered_search.set('milestone:%100 milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 283814d2cbb..e72417aea80 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Filter issues', feature: true do +describe 'Filter issues', js: true, feature: true do include WaitForAjax let!(:group) { create(:group) } @@ -98,7 +98,7 @@ describe 'Filter issues', feature: true do end describe 'filter issues by author' do - context 'only author', js: true do + context 'only author' do it 'filters issues by searched author' do input_filtered_search("author:@#{user.username}") expect_issues_list_count(5) @@ -113,7 +113,7 @@ describe 'Filter issues', feature: true do end end - context 'author with other filters', js: true do + context 'author with other filters' do it 'filters issues by searched author and text' do search = "author:@#{user.username} issue" input_filtered_search(search) @@ -143,13 +143,13 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do # TODO end end describe 'filter issues by assignee' do - context 'only assignee', js: true do + context 'only assignee' do it 'filters issues by searched assignee' do search = "assignee:@#{user.username}" input_filtered_search(search) @@ -173,7 +173,7 @@ describe 'Filter issues', feature: true do end end - context 'assignee with other filters', js: true do + context 'assignee with other filters' do it 'filters issues by searched assignee and text' do search = "assignee:@#{user.username} searchTerm" input_filtered_search(search) @@ -203,13 +203,13 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do # TODO end end describe 'filter issues by label' do - context 'only label', js: true do + context 'only label' do it 'filters issues by searched label' do search = "label:~#{bug_label.title}" input_filtered_search(search) @@ -256,7 +256,7 @@ describe 'Filter issues', feature: true do end end - context 'label with multiple words', js: true do + context 'label with multiple words' do it 'special characters' do special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce") special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) @@ -308,7 +308,7 @@ describe 'Filter issues', feature: true do end end - context 'label with other filters', js: true do + context 'label with other filters' do it 'filters issues by searched label and text' do search = "label:~#{caps_sensitive_label.title} bug" input_filtered_search(search) @@ -338,7 +338,7 @@ describe 'Filter issues', feature: true do end end - context 'multiple labels with other filters', js: true do + context 'multiple labels with other filters' do it 'filters issues by searched label, label2, and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" input_filtered_search(search) @@ -368,7 +368,7 @@ describe 'Filter issues', feature: true do end end - context 'issue label clicked', js: true do + context 'issue label clicked' do before do find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click sleep 1 @@ -383,13 +383,13 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do # TODO end end describe 'filter issues by milestone' do - context 'only milestone', js: true do + context 'only milestone' do it 'filters issues by searched milestone' do input_filtered_search("milestone:%#{milestone.title}") expect_issues_list_count(5) @@ -433,7 +433,7 @@ describe 'Filter issues', feature: true do end end - context 'milestone with other filters', js: true do + context 'milestone with other filters' do it 'filters issues by searched milestone and text' do search = "milestone:%#{milestone.title} bug" input_filtered_search(search) @@ -463,13 +463,13 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do # TODO end end describe 'filter issues by text' do - context 'only text', js: true do + context 'only text' do it 'filters issues by searched text' do search = 'Bug' input_filtered_search(search) @@ -520,7 +520,7 @@ describe 'Filter issues', feature: true do end end - context 'searched text with other filters', js: true do + context 'searched text with other filters' do it 'filters issues by searched text and author' do input_filtered_search("bug author:@#{user.username}") expect_issues_list_count(2) @@ -588,7 +588,7 @@ describe 'Filter issues', feature: true do end end - context 'sorting', js: true do + context 'sorting' do it 'sorts by oldest updated' do create(:issue, title: '3 days ago', @@ -618,7 +618,7 @@ describe 'Filter issues', feature: true do end end - describe 'retains filter when switching issue states', js: true do + describe 'retains filter when switching issue states' do before do input_filtered_search('bug') diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index d37057a44f8..3af36ae27dd 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -15,9 +15,9 @@ describe 'Search bar', js: true, feature: true do visit namespace_project_issues_path(project.namespace, project) end - def getLeftStyle(style) - leftStyle = /left:\s\d*[.]\d*px/.match(style) - leftStyle.to_s.gsub('left: ', '').to_f; + def get_left_style(style) + left_style = /left:\s\d*[.]\d*px/.match(style) + left_style.to_s.gsub('left: ', '').to_f end describe 'clear search button' do @@ -53,7 +53,7 @@ describe 'Search bar', js: true, feature: true do end it 'resets the dropdown hint filter' do - filtered_search.click(); + filtered_search.click() original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size filtered_search.set('author') @@ -66,8 +66,8 @@ describe 'Search bar', js: true, feature: true do it 'resets the dropdown filters' do filtered_search.set('a') - hintStyle = page.find('#js-dropdown-hint')['style'] - hintOffset = getLeftStyle(hintStyle) + hint_style = page.find('#js-dropdown-hint')['style'] + hint_offset = get_left_style(hint_style) filtered_search.set('author:') expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) @@ -75,7 +75,7 @@ describe 'Search bar', js: true, feature: true do find('.filtered-search-input-container .clear-search').click filtered_search.click() expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 - expect(getLeftStyle(page.find('#js-dropdown-hint')['style'])).to eq (hintOffset) + expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq hint_offset end end end From 0e40c952d6d715580ed0ec891dc6f4fdc810673e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 16 Dec 2016 18:19:54 -0600 Subject: [PATCH 162/185] Remove unused finder variable --- app/views/shared/issuable/_search_bar.html.haml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 896769768eb..3449c1f0151 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,6 +1,3 @@ -- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder -- boards_page = controller.controller_name == 'boards' - .issues-filters .issues-details-filters.row-content-block.second-block.filtered-search-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do From e197f27f19ab7995d280f67754ea16c2629701b2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 17 Dec 2016 13:22:00 -0600 Subject: [PATCH 163/185] Refactor and use regex for string processing --- .../filtered_search/dropdown_hint.js.es6 | 58 ++-- .../filtered_search/dropdown_user.js.es6 | 9 +- .../filtered_search/dropdown_utils.js.es6 | 30 +- .../filtered_search_dropdown.js.es6 | 2 +- .../filtered_search_dropdown_manager.js.es6 | 34 +- .../filtered_search_manager.js.es6 | 14 +- .../filtered_search_tokenizer.js.es6 | 174 ++-------- .../dropdown_utils_spec.js.es6 | 25 +- ...ltered_search_dropdown_manager_spec.js.es6 | 22 +- .../filtered_search_tokenizer_spec.js.es6 | 309 ++++-------------- 10 files changed, 165 insertions(+), 512 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index 7bf30143d78..c5ab9c52d76 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -3,31 +3,13 @@ /* global droplabFilter */ (() => { - const dropdownData = [{ - icon: 'fa-pencil', - hint: 'author:', - tag: '<author>', - }, { - icon: 'fa-user', - hint: 'assignee:', - tag: '<assignee>', - }, { - icon: 'fa-clock-o', - hint: 'milestone:', - tag: '<milestone>', - }, { - icon: 'fa-tag', - hint: 'label:', - tag: '<label>', - }]; - class DropdownHint extends gl.FilteredSearchDropdown { constructor(droplab, dropdown, input) { super(droplab, dropdown, input); this.config = { droplabFilter: { template: 'hint', - filterFunction: gl.DropdownUtils.filterMethod, + filterFunction: gl.DropdownUtils.filterHint, }, }; } @@ -43,8 +25,7 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchDropdownManager - .addWordToInput(this.getSelectedTextWithoutEscaping(token)); + gl.FilteredSearchDropdownManager.addWordToInput(token); } this.dismissDropdown(); this.dispatchInputEvent(); @@ -52,24 +33,27 @@ } } - getSelectedTextWithoutEscaping(selectedToken) { - const lastWord = this.input.value.split(' ').last(); - const lastWordIndex = selectedToken.indexOf(lastWord); - - return lastWordIndex === -1 ? selectedToken : selectedToken.slice(lastWord.length); - } - renderContent() { + const dropdownData = [{ + icon: 'fa-pencil', + hint: 'author:', + tag: '<author>', + }, { + icon: 'fa-user', + hint: 'assignee:', + tag: '<assignee>', + }, { + icon: 'fa-clock-o', + hint: 'milestone:', + tag: '<milestone>', + }, { + icon: 'fa-tag', + hint: 'label:', + tag: '<label>', + }]; + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); - - // Clone dropdownData to prevent it from being - // changed due to pass by reference - const data = []; - dropdownData.forEach((item) => { - data.push(Object.assign({}, item)); - }); - - this.droplab.setData(this.hookId, data); + this.droplab.setData(this.hookId, dropdownData); } init() { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 7a566907312..49581e3bfbd 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -37,13 +37,10 @@ } getSearchInput() { - const query = this.input.value; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1); - const hasPrefix = valueWithoutColon[0] === '@'; - const valueWithoutPrefix = valueWithoutColon.slice(1); + const query = this.input.value.trim(); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - return hasPrefix ? valueWithoutPrefix : valueWithoutColon; + return lastToken.value || ''; } init() { diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index 3837b020fd3..d246000ff52 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -22,30 +22,32 @@ static filterWithSymbol(filterSymbol, item, query) { const updatedItem = item; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); - const valueWithoutColon = value.slice(1).toLowerCase(); - const prefix = valueWithoutColon[0]; - const valueWithoutPrefix = valueWithoutColon.slice(1); + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); - const title = updatedItem.title.toLowerCase(); + if (lastToken !== searchToken) { + const value = lastToken.value.toLowerCase(); + const title = updatedItem.title.toLowerCase(); - // Eg. filterSymbol = ~ for labels - const matchWithoutPrefix = - prefix === filterSymbol && title.indexOf(valueWithoutPrefix) !== -1; - const match = title.indexOf(valueWithoutColon) !== -1; + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + } else { + updatedItem.droplab_hidden = false; + } - updatedItem.droplab_hidden = !match && !matchWithoutPrefix; return updatedItem; } - static filterMethod(item, query) { + static filterHint(item, query) { const updatedItem = item; - const { value } = gl.FilteredSearchTokenizer.getLastTokenObject(query); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - if (value === '') { + if (!lastToken) { updatedItem.droplab_hidden = false; } else { - updatedItem.droplab_hidden = updatedItem.hint.indexOf(value) === -1; + updatedItem.droplab_hidden = updatedItem.hint.indexOf(lastToken.toLowerCase()) === -1; } return updatedItem; 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 68014e27462..56147ad93c9 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -29,7 +29,7 @@ itemClicked(e, getValueFunction) { const { selected } = e.detail; - if (selected.tagName === 'LI') { + if (selected.tagName === 'LI' && selected.innerHTML) { const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); if (!dataValueSet) { 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 ac71b5e4434..b67176267fb 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 @@ -57,17 +57,25 @@ static addWordToInput(word, addSpace = false) { const input = document.querySelector('.filtered-search'); + input.value = input.value.trim(); + const value = input.value; const hasExistingValue = value.length !== 0; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(value); + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(value); + + // Find out what part of the token value the user has typed + // and remove it from input before appending the selected token value + if (lastToken !== searchToken) { + const lastTokenString = `${lastToken.symbol}${lastToken.value}`; - if ({}.hasOwnProperty.call(lastToken, 'key')) { // Spaces inside the token means that the token value will be escaped by quotes - const hasQuotes = lastToken.value.indexOf(' ') !== -1; + const hasQuotes = lastTokenString.indexOf(' ') !== -1; // Add 2 length to account for the length of the front and back quotes - const lengthToRemove = hasQuotes ? lastToken.value.length + 2 : lastToken.value.length; + const lengthToRemove = hasQuotes ? lastTokenString.length + 2 : lastTokenString.length; input.value = value.slice(0, -1 * (lengthToRemove)); + } else if (searchToken !== '' && word.indexOf(searchToken) !== -1) { + input.value = value.slice(0, -1 * searchToken.length); } input.value += hasExistingValue && addSpace ? ` ${word}` : word; @@ -129,27 +137,25 @@ const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key - && {}.hasOwnProperty.call(this.mapping, match.key); + && this.mapping[match.key]; const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { - // `hint` is not listed as a tokenKey (since it is not a real `filter`) - const key = match && {}.hasOwnProperty.call(match, 'key') ? match.key : 'hint'; + const key = match && match.key ? match.key : 'hint'; this.load(key, firstLoad); } - - gl.droplab = this.droplab; } setDropdown() { - const { lastToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + const { lastToken, searchToken } = this.tokenizer + .processTokens(this.filteredSearchInput.value); - if (typeof lastToken === 'string') { + if (lastToken === searchToken) { // Token is not fully initialized yet because it has no value // Eg. token = 'label:' - const { tokenKey } = this.tokenizer.parseToken(lastToken); - this.loadDropdown(tokenKey); - } else if ({}.hasOwnProperty.call(lastToken, 'key')) { + const split = lastToken.split(':'); + this.loadDropdown(split.length > 1 ? split[0] : ''); + } else if (lastToken) { // Token has been initialized into an object because it has a value this.loadDropdown(lastToken.key); } else { 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 565f2347072..d2ea4de18aa 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -136,21 +136,13 @@ const condition = gl.FilteredSearchTokenKeys .searchByConditionKeyValue(token.key, token.value.toLowerCase()); const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); + const keyParam = param ? `${token.key}_${param}` : token.key; let tokenPath = ''; - let keyParam = token.key; - if (param) { - keyParam += `_${param}`; - } - - if (token.wildcard && condition) { + if (condition) { tokenPath = condition.url; - } else if (token.wildcard) { - // wildcard means that the token does not have a symbol - tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`; } else { - // Remove the token symbol - tokenPath = `${keyParam}=${encodeURIComponent(token.value.slice(1))}`; + tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`; } paths.push(tokenPath); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index 57c0e8fc359..14ca78e139b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -1,165 +1,39 @@ (() => { class FilteredSearchTokenizer { - static parseToken(input) { - const colonIndex = input.indexOf(':'); - let tokenKey; - let tokenValue; - let tokenSymbol; - - if (colonIndex !== -1) { - tokenKey = input.slice(0, colonIndex).toLowerCase(); - tokenValue = input.slice(colonIndex + 1); - tokenSymbol = tokenValue[0]; - } - - return { - tokenKey, - tokenValue, - tokenSymbol, - }; - } - - static getLastTokenObject(input) { - const token = FilteredSearchTokenizer.getLastToken(input); - const colonIndex = token.indexOf(':'); - - const key = colonIndex !== -1 ? token.slice(0, colonIndex) : ''; - const value = colonIndex !== -1 ? token.slice(colonIndex) : token; - - return { - key, - value, - }; - } - - static getLastToken(input) { - let completeToken = false; - let completeQuotation = true; - let lastQuotation = ''; - let i = input.length; - - const doubleQuote = '"'; - const singleQuote = '\''; - while (!completeToken && i >= 0) { - const isDoubleQuote = input[i] === doubleQuote; - const isSingleQuote = input[i] === singleQuote; - - // If the second quotation is found - if ((lastQuotation === doubleQuote && isDoubleQuote) || - (lastQuotation === singleQuote && isSingleQuote)) { - completeQuotation = true; - } - - // Save the first quotation - if ((isDoubleQuote && lastQuotation === '') || - (isSingleQuote && lastQuotation === '')) { - lastQuotation = input[i]; - completeQuotation = false; - } - - if (completeQuotation && input[i] === ' ') { - completeToken = true; - } else { - i -= 1; - } - } - - // Adjust by 1 because of empty space - return input.slice(i + 1); - } - static processTokens(input) { + const tokenRegex = /(\w+):([~%@]?)(?:"(.*?)"|'(.*?)'|(\S+))/g; const tokens = []; - let searchToken = ''; - let lastToken = ''; + let lastToken = null; + const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { + let tokenValue = v1 || v2 || v3; + let tokenSymbol = symbol; - const inputs = input.split(' '); - let searchTerms = ''; - let lastQuotation = ''; - let incompleteToken = false; - - // Iterate through each word (broken up by spaces) - inputs.forEach((i) => { - if (incompleteToken) { - // Continue previous token as it had an escaped - // quote in the beginning - const prevToken = tokens.last(); - prevToken.value += ` ${i}`; - - // Remove last quotation from the value - const lastQuotationRegex = new RegExp(lastQuotation, 'g'); - prevToken.value = prevToken.value.replace(lastQuotationRegex, ''); - tokens[tokens.length - 1] = prevToken; - - // Check to see if this quotation completes the token value - if (i.indexOf(lastQuotation) !== -1) { - lastToken = tokens.last(); - incompleteToken = !incompleteToken; - } - - return; + if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { + tokenSymbol = tokenValue; + tokenValue = ''; } - const colonIndex = i.indexOf(':'); + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + return ''; + }).replace(/\s{2,}/g, ' ').trim() || ''; - if (colonIndex !== -1) { - const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer.parseToken(i); - - const keyMatch = gl.FilteredSearchTokenKeys.searchByKey(tokenKey); - const symbolMatch = gl.FilteredSearchTokenKeys.searchBySymbol(tokenSymbol); - - const doubleQuoteOccurrences = tokenValue.split('"').length - 1; - const singleQuoteOccurrences = tokenValue.split('\'').length - 1; - - const doubleQuoteIndex = tokenValue.indexOf('"'); - const singleQuoteIndex = tokenValue.indexOf('\''); - - const doubleQuoteExist = doubleQuoteIndex !== -1; - const singleQuoteExist = singleQuoteIndex !== -1; - - const doubleQuoteExistOnly = doubleQuoteExist && !singleQuoteExist; - const doubleQuoteIsBeforeSingleQuote = - doubleQuoteExist && singleQuoteExist && doubleQuoteIndex < singleQuoteIndex; - - const singleQuoteExistOnly = singleQuoteExist && !doubleQuoteExist; - const singleQuoteIsBeforeDoubleQuote = - doubleQuoteExist && singleQuoteExist && singleQuoteIndex < doubleQuoteIndex; - - if ((doubleQuoteExistOnly || doubleQuoteIsBeforeSingleQuote) - && doubleQuoteOccurrences % 2 !== 0) { - // " is found and is in front of ' (if any) - lastQuotation = '"'; - incompleteToken = true; - } else if ((singleQuoteExistOnly || singleQuoteIsBeforeDoubleQuote) - && singleQuoteOccurrences % 2 !== 0) { - // ' is found and is in front of " (if any) - lastQuotation = '\''; - incompleteToken = true; - } - - if (keyMatch && tokenValue.length > 0) { - tokens.push({ - key: keyMatch.key, - value: tokenValue, - wildcard: !symbolMatch, - }); - lastToken = tokens.last(); - - return; - } - } - - // Add space for next term - searchTerms += `${i} `; - lastToken = i; - }, this); - - searchToken = searchTerms.trim(); + if (tokens.length > 0) { + const last = tokens[tokens.length - 1]; + const lastString = `${last.key}:${last.symbol}${last.value}`; + lastToken = input.lastIndexOf(lastString) === + input.length - lastString.length ? last : searchToken; + } else { + lastToken = searchToken; + } return { tokens, - searchToken, lastToken, + searchToken, }; } } diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 index 07293b9f877..369eb90e31c 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -34,11 +34,6 @@ title: '@root', }; - beforeEach(() => { - spyOn(gl.FilteredSearchTokenizer, 'getLastTokenObject') - .and.callFake(query => ({ value: query })); - }); - it('should filter without symbol', () => { const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo'); expect(updatedItem.droplab_hidden).toBe(false); @@ -49,37 +44,27 @@ expect(updatedItem.droplab_hidden).toBe(false); }); - it('should filter with invalid symbol', () => { - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':#'); - expect(updatedItem.droplab_hidden).toBe(true); - }); - it('should filter with colon', () => { const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':'); expect(updatedItem.droplab_hidden).toBe(false); }); }); - describe('filterMethod', () => { - beforeEach(() => { - spyOn(gl.FilteredSearchTokenizer, 'getLastTokenObject') - .and.callFake(query => ({ value: query })); - }); - - it('should filter by hint', () => { - let updatedItem = gl.DropdownUtils.filterMethod({ + describe('filterHint', () => { + it('should filter', () => { + let updatedItem = gl.DropdownUtils.filterHint({ hint: 'label', }, 'l'); expect(updatedItem.droplab_hidden).toBe(false); - updatedItem = gl.DropdownUtils.filterMethod({ + updatedItem = gl.DropdownUtils.filterHint({ hint: 'label', }, 'o'); expect(updatedItem.droplab_hidden).toBe(true); }); it('should return droplab_hidden false when item has no hint', () => { - const updatedItem = gl.DropdownUtils.filterMethod({}, ''); + const updatedItem = gl.DropdownUtils.filterHint({}, ''); expect(updatedItem.droplab_hidden).toBe(false); }); }); 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 17d414aaad1..562673a4ee5 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 @@ -21,13 +21,6 @@ }); describe('input has no existing value', () => { - beforeEach(() => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens') - .and.callFake(() => ({ - lastToken: {}, - })); - }); - it('should add word', () => { gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); expect(getInputValue()).toBe('firstWord'); @@ -61,26 +54,13 @@ value: 'roo', }; - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.callFake(() => ({ - lastToken, - })); - document.querySelector('.filtered-search').value = `${lastToken.key}:${lastToken.value}`; gl.FilteredSearchDropdownManager.addWordToInput('root'); expect(getInputValue()).toBe('author:root'); }); it('should only add the remaining characters of the word (contains space)', () => { - const lastToken = { - key: 'label', - value: 'test me', - }; - - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.callFake(() => ({ - lastToken, - })); - - document.querySelector('.filtered-search').value = `${lastToken.key}:"${lastToken.value}"`; + document.querySelector('.filtered-search').value = 'label:~"test'; gl.FilteredSearchDropdownManager.addWordToInput('~\'"test me"\''); expect(getInputValue()).toBe('label:~\'"test me"\''); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index c93f163e763..8e5992fa446 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -4,267 +4,100 @@ (() => { describe('Filtered Search Tokenizer', () => { - describe('parseToken', () => { - it('should return key, value and symbol', () => { - const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer - .parseToken('author:@user'); - - expect(tokenKey).toBe('author'); - expect(tokenValue).toBe('@user'); - expect(tokenSymbol).toBe('@'); - }); - - it('should return value with spaces', () => { - const { tokenKey, tokenValue, tokenSymbol } = gl.FilteredSearchTokenizer - .parseToken('label:~"test me"'); - - expect(tokenKey).toBe('label'); - expect(tokenValue).toBe('~"test me"'); - expect(tokenSymbol).toBe('~'); - }); - }); - - describe('getLastTokenObject', () => { - beforeEach(() => { - spyOn(gl.FilteredSearchTokenizer, 'getLastToken').and.callFake(input => input); - }); - - it('should return key and value', () => { - const { key, value } = gl.FilteredSearchTokenizer.getLastTokenObject('author:@root'); - expect(key).toBe('author'); - expect(value).toBe(':@root'); - }); - - describe('string without colon', () => { - let lastTokenObject; - - beforeEach(() => { - lastTokenObject = gl.FilteredSearchTokenizer.getLastTokenObject('author'); - }); - - it('should return key as an empty string', () => { - expect(lastTokenObject.key).toBe(''); - }); - - it('should return input as value', () => { - expect(lastTokenObject.value).toBe('author'); - }); - }); - }); - - describe('getLastToken', () => { - it('returns entire string when there is only one word', () => { - const lastToken = gl.FilteredSearchTokenizer.getLastToken('input'); - expect(lastToken).toBe('input'); - }); - - it('returns last word when there are multiple words', () => { - const lastToken = gl.FilteredSearchTokenizer.getLastToken('this is a few words'); - expect(lastToken).toBe('words'); - }); - - it('returns last token when there are multiple tokens', () => { - const lastToken = gl.FilteredSearchTokenizer - .getLastToken('label:fun author:root milestone:2.0'); - expect(lastToken).toBe('milestone:2.0'); - }); - - it('returns last token containing spaces escaped by double quotes', () => { - const lastToken = gl.FilteredSearchTokenizer - .getLastToken('label:fun author:root milestone:2.0 label:~"Feature Proposal"'); - expect(lastToken).toBe('label:~"Feature Proposal"'); - }); - - it('returns last token containing spaces escaped by single quotes', () => { - const lastToken = gl.FilteredSearchTokenizer - .getLastToken('label:fun author:root milestone:2.0 label:~\'Feature Proposal\''); - expect(lastToken).toBe('label:~\'Feature Proposal\''); - }); - - it('returns last token containing special characters', () => { - const lastToken = gl.FilteredSearchTokenizer - .getLastToken('label:fun author:root milestone:2.0 label:~!@#$%^&*()'); - expect(lastToken).toBe('label:~!@#$%^&*()'); - }); - }); - describe('processTokens', () => { - describe('input does not contain any tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); - }); - - it('returns input as searchToken', () => { - expect(results.searchToken).toBe('searchTerm'); - }); - - it('returns tokens as an empty array', () => { - expect(results.tokens.length).toBe(0); - }); - - it('returns lastToken equal to searchToken', () => { - expect(results.lastToken).toBe(results.searchToken); - }); + it('returns for input containing only search value', () => { + const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(0); + expect(results.lastToken).toBe(results.searchToken); }); - describe('input contains only tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); - }); + it('returns for input containing only tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + expect(results.searchToken).toBe(''); + expect(results.tokens.length).toBe(4); + expect(results.tokens[3]).toBe(results.lastToken); - it('returns searchToken as an empty string', () => { - expect(results.searchToken).toBe(''); - }); + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); - it('returns tokens array of size equal to the number of tokens in input', () => { - expect(results.tokens.length).toBe(4); - }); + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('Very Important'); + expect(results.tokens[1].symbol).toBe('~'); - it('returns tokens array that matches the tokens found in input', () => { - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('@root'); - expect(results.tokens[0].wildcard).toBe(false); + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('v1.0'); + expect(results.tokens[2].symbol).toBe('%'); - expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('~Very Important'); - expect(results.tokens[1].wildcard).toBe(false); - - expect(results.tokens[2].key).toBe('milestone'); - expect(results.tokens[2].value).toBe('%v1.0'); - expect(results.tokens[2].wildcard).toBe(false); - - expect(results.tokens[3].key).toBe('assignee'); - expect(results.tokens[3].value).toBe('none'); - expect(results.tokens[3].wildcard).toBe(true); - }); - - it('returns lastToken equal to the last object in the tokens array', () => { - expect(results.tokens[3]).toBe(results.lastToken); - }); + expect(results.tokens[3].key).toBe('assignee'); + expect(results.tokens[3].value).toBe('none'); + expect(results.tokens[3].symbol).toBe(''); }); - describe('input starts with search value and ends with tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('searchTerm anotherSearchTerm milestone:none'); - }); - - it('returns searchToken', () => { - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - }); - - it('returns correct number of tokens', () => { - expect(results.tokens.length).toBe(1); - }); - - it('returns correct tokens', () => { - expect(results.tokens[0].key).toBe('milestone'); - expect(results.tokens[0].value).toBe('none'); - expect(results.tokens[0].wildcard).toBe(true); - }); - - it('returns lastToken', () => { - expect(results.tokens[0]).toBe(results.lastToken); - }); + it('returns for input starting with search value and ending with tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('searchTerm anotherSearchTerm milestone:none'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0]).toBe(results.lastToken); + expect(results.tokens[0].key).toBe('milestone'); + expect(results.tokens[0].value).toBe('none'); + expect(results.tokens[0].symbol).toBe(''); }); - describe('input starts with token and ends with search value', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('assignee:@user searchTerm'); - }); + it('returns for input starting with tokens and ending with search value', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('assignee:@user searchTerm'); - it('returns searchToken', () => { - expect(results.searchToken).toBe('searchTerm'); - }); - - it('returns correct number of tokens', () => { - expect(results.tokens.length).toBe(1); - }); - - it('returns correct tokens', () => { - expect(results.tokens[0].key).toBe('assignee'); - expect(results.tokens[0].value).toBe('@user'); - expect(results.tokens[0].wildcard).toBe(false); - }); - - it('returns lastToken as the searchTerm', () => { - expect(results.lastToken).toBe(results.searchToken); - }); + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('assignee'); + expect(results.tokens[0].value).toBe('user'); + expect(results.tokens[0].symbol).toBe('@'); + expect(results.lastToken).toBe(results.searchToken); }); - describe('input contains search value wrapped between tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); - }); + it('returns for input containing search value wrapped between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); - it('returns searchToken', () => { - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - }); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); - it('returns correct number of tokens', () => { - expect(results.tokens.length).toBe(3); - }); + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('Won\'t fix'); + expect(results.tokens[1].symbol).toBe('~'); - it('returns tokens array in the order it was processed', () => { - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('@root'); - expect(results.tokens[0].wildcard).toBe(false); - - expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('~Won\'t fix'); - expect(results.tokens[1].wildcard).toBe(false); - - expect(results.tokens[2].key).toBe('milestone'); - expect(results.tokens[2].value).toBe('none'); - expect(results.tokens[2].wildcard).toBe(true); - }); - - it('returns lastToken', () => { - expect(results.tokens[2]).toBe(results.lastToken); - }); + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('none'); + expect(results.tokens[2].symbol).toBe(''); }); - describe('input search value is spaced in between tokens', () => { - let results; - beforeEach(() => { - results = gl.FilteredSearchTokenizer - .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); - }); + it('returns for input containing search value in between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); - it('returns searchToken', () => { - expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); - }); + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); - it('returns correct number of tokens', () => { - expect(results.tokens.length).toBe(3); - }); + expect(results.tokens[1].key).toBe('assignee'); + expect(results.tokens[1].value).toBe('none'); + expect(results.tokens[1].symbol).toBe(''); - it('returns tokens array in the order it was processed', () => { - expect(results.tokens[0].key).toBe('author'); - expect(results.tokens[0].value).toBe('@root'); - expect(results.tokens[0].wildcard).toBe(false); - - expect(results.tokens[1].key).toBe('assignee'); - expect(results.tokens[1].value).toBe('none'); - expect(results.tokens[1].wildcard).toBe(true); - - expect(results.tokens[2].key).toBe('label'); - expect(results.tokens[2].value).toBe('~Doing'); - expect(results.tokens[2].wildcard).toBe(false); - }); - - it('returns lastToken', () => { - expect(results.tokens[2]).toBe(results.lastToken); - }); + expect(results.tokens[2].key).toBe('label'); + expect(results.tokens[2].value).toBe('Doing'); + expect(results.tokens[2].symbol).toBe('~'); }); }); }); From f72c1bf1c930b4dcb533202204d132f42246d99f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sat, 17 Dec 2016 19:55:44 -0600 Subject: [PATCH 164/185] Fix specs --- .../filtered_search/dropdown_utils.js.es6 | 13 +++++++++---- .../filtered_search_dropdown_manager.js.es6 | 15 +++++++++++---- .../filtered_search/dropdown_assignee_spec.rb | 6 +++--- .../filtered_search/dropdown_author_spec.rb | 8 +++++--- .../issues/filtered_search/dropdown_label_spec.rb | 2 +- .../issues/filtered_search/filter_issues_spec.rb | 2 +- 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index d246000ff52..0c0d24d4de8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -42,12 +42,17 @@ static filterHint(item, query) { const updatedItem = item; - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + lastToken = lastToken || ''; - if (!lastToken) { + if (!lastToken || query.split('').last() === ' ') { updatedItem.droplab_hidden = false; - } else { - updatedItem.droplab_hidden = updatedItem.hint.indexOf(lastToken.toLowerCase()) === -1; + } else if (lastToken) { + const split = lastToken.split(':'); + const tokenName = split[0].split(' ').last(); + + const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; + updatedItem.droplab_hidden = tokenName ? match : false; } return updatedItem; 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 b67176267fb..e9f1fbf63ed 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 @@ -62,6 +62,7 @@ const value = input.value; const hasExistingValue = value.length !== 0; const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(value); + const lastSearchToken = searchToken.split(' ').last(); // Find out what part of the token value the user has typed // and remove it from input before appending the selected token value @@ -74,8 +75,8 @@ // Add 2 length to account for the length of the front and back quotes const lengthToRemove = hasQuotes ? lastTokenString.length + 2 : lastTokenString.length; input.value = value.slice(0, -1 * (lengthToRemove)); - } else if (searchToken !== '' && word.indexOf(searchToken) !== -1) { - input.value = value.slice(0, -1 * searchToken.length); + } else if (searchToken !== '' && word.indexOf(lastSearchToken) !== -1) { + input.value = value.slice(0, -1 * lastSearchToken.length); } input.value += hasExistingValue && addSpace ? ` ${word}` : word; @@ -150,11 +151,17 @@ const { lastToken, searchToken } = this.tokenizer .processTokens(this.filteredSearchInput.value); - if (lastToken === searchToken) { + if (this.filteredSearchInput.value.split('').last() === ' ') { + this.updateCurrentDropdownOffset(); + } + + if (lastToken === searchToken && lastToken !== null) { // Token is not fully initialized yet because it has no value // Eg. token = 'label:' + const split = lastToken.split(':'); - this.loadDropdown(split.length > 1 ? split[0] : ''); + const dropdownName = split[0].split(' ').last(); + this.loadDropdown(split.length > 1 ? dropdownName : ''); } else if (lastToken) { // Token has been initialized into an object because it has a value this.loadDropdown(lastToken.key); diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 5d0e95580c9..a5a05a622b6 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -13,7 +13,7 @@ describe 'Dropdown assignee', js: true, feature: true do def send_keys_to_filtered_search(input) input.split("").each do |i| filtered_search.send_keys(i) - sleep 3 + sleep 5 wait_for_ajax end end @@ -65,7 +65,7 @@ describe 'Dropdown assignee', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('assignee:') + send_keys_to_filtered_search('assignee:') end it 'filters by name' do @@ -118,7 +118,7 @@ describe 'Dropdown assignee', js: true, feature: true do end it 'selects `no assignee`' do - click_assignee('No Assignee') + find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click expect(page).to have_css(js_dropdown_assignee, visible: false) expect(filtered_search.value).to eq("assignee:none") end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index c7cb5e25174..956f7156c5f 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -13,7 +13,7 @@ describe 'Dropdown author', js: true, feature: true do def send_keys_to_filtered_search(input) input.split("").each do |i| filtered_search.send_keys(i) - sleep 3 + sleep 5 wait_for_ajax end end @@ -65,7 +65,8 @@ describe 'Dropdown author', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('author:') + filtered_search.set('author') + send_keys_to_filtered_search(':') end it 'filters by name' do @@ -101,7 +102,8 @@ describe 'Dropdown author', js: true, feature: true do describe 'selecting from dropdown' do before do - filtered_search.set('author:') + filtered_search.set('author') + send_keys_to_filtered_search(':') end it 'fills in the author username when the author has not been filtered' do diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index d007e160b3e..a0e2973d6b8 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -176,7 +176,7 @@ describe 'Dropdown label', js: true, feature: true do end it 'selects `no label`' do - click_label('No Label') + find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:none") end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index e72417aea80..ed6d4a0787a 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -17,9 +17,9 @@ describe 'Filter issues', js: true, feature: true do let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } 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) - filtered_search = find('.filtered-search') filtered_search.set(search_term) filtered_search.send_keys(:enter) end From 12753def903f265467d2cab8e19deced31daf066 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Sun, 18 Dec 2016 17:04:29 -0600 Subject: [PATCH 165/185] Fix specs --- app/assets/javascripts/dispatcher.js.es6 | 2 +- .../filtered_search/dropdown_utils.js.es6 | 2 +- .../filtered_search/dropdown_author_spec.rb | 13 ++---- .../filtered_search/dropdown_label_spec.rb | 40 +++++++++++-------- .../dropdown_milestone_spec.rb | 39 ++++-------------- ...ltered_search_dropdown_manager_spec.js.es6 | 2 - 6 files changed, 36 insertions(+), 62 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 1e9111f4718..ca2da18dc26 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,7 +84,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if(document.querySelector('.filtered-search') && gl.FilteredSearchManager) { + if (document.querySelector('.filtered-search') && gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index 0c0d24d4de8..3453311bee5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -43,7 +43,7 @@ static filterHint(item, query) { const updatedItem = item; let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); - lastToken = lastToken || ''; + lastToken = lastToken.key || lastToken || ''; if (!lastToken || query.split('').last() === ' ') { updatedItem.droplab_hidden = false; diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 956f7156c5f..d053b032768 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -70,13 +70,13 @@ describe 'Dropdown author', js: true, feature: true do end it 'filters by name' do - send_keys_to_filtered_search('j') - expect(dropdown_author_size).to eq(2) + send_keys_to_filtered_search('ja') + expect(dropdown_author_size).to eq(1) end it 'filters by case insensitive name' do - send_keys_to_filtered_search('J') - expect(dropdown_author_size).to eq(2) + send_keys_to_filtered_search('Ja') + expect(dropdown_author_size).to eq(1) end it 'filters by username with symbol' do @@ -84,11 +84,6 @@ describe 'Dropdown author', js: true, feature: true do expect(dropdown_author_size).to eq(2) end - it 'filters by case insensitive username with symbol' do - send_keys_to_filtered_search('@OT') - expect(dropdown_author_size).to eq(2) - end - it 'filters by username without symbol' do send_keys_to_filtered_search('ot') expect(dropdown_author_size).to eq(2) diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index a0e2973d6b8..1a21b0d64ed 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -9,6 +9,7 @@ describe 'Dropdown label', js: true, feature: true do let!(:uppercase_label) { create(:label, project: project, title: 'BUG') } let!(:two_words_label) { create(:label, project: project, title: 'High Priority') } let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') } + let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') } let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')} let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')} let(:filtered_search) { find('.filtered-search') } @@ -68,61 +69,66 @@ describe 'Dropdown label', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('label:') + filtered_search.set('label') end it 'filters by name' do - send_keys_to_filtered_search('b') + send_keys_to_filtered_search(':b') expect(dropdown_label_size).to eq(2) end it 'filters by case insensitive name' do - send_keys_to_filtered_search('B') + send_keys_to_filtered_search(':B') expect(dropdown_label_size).to eq(2) end it 'filters by name with symbol' do - send_keys_to_filtered_search('~bu') + send_keys_to_filtered_search(':~bu') expect(dropdown_label_size).to eq(2) end it 'filters by case insensitive name with symbol' do - send_keys_to_filtered_search('~BU') + send_keys_to_filtered_search(':~BU') expect(dropdown_label_size).to eq(2) end - it 'filters by multiple names using double quotes' do - send_keys_to_filtered_search('"High P') + it 'filters by multiple words' do + send_keys_to_filtered_search(':Hig') expect(dropdown_label_size).to eq(1) end - it 'filters by multiple names using single quotes' do - send_keys_to_filtered_search('\'High P') + it 'filters by multiple words with symbol' do + send_keys_to_filtered_search(':~Hig') expect(dropdown_label_size).to eq(1) end - it 'filters by multiple names using single and double quotes' do - send_keys_to_filtered_search('~"won`\'t f') + it 'filters by multiple words containing single quotes' do + send_keys_to_filtered_search(':won\'t') expect(dropdown_label_size).to eq(1) end - it 'filters by multiple names using double quotes with symbol' do - send_keys_to_filtered_search('~"High P') + it 'filters by multiple words containing single quotes with symbol' do + send_keys_to_filtered_search(':~won\'t') expect(dropdown_label_size).to eq(1) end - it 'filters by multiple names using single quotes with symbol' do - send_keys_to_filtered_search('~\'High P') + it 'filters by multiple words containing double quotes' do + send_keys_to_filtered_search(':won"t') + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing double quotes with symbol' do + send_keys_to_filtered_search(':~won"t') expect(dropdown_label_size).to eq(1) end it 'filters by special characters' do - send_keys_to_filtered_search('^+') + send_keys_to_filtered_search(':^+') expect(dropdown_label_size).to eq(1) end it 'filters by special characters with symbol' do - send_keys_to_filtered_search('~^+') + send_keys_to_filtered_search(':~^+') expect(dropdown_label_size).to eq(1) end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index d367430a670..64fc83c6ccb 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -73,61 +73,36 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'filtering' do before do - filtered_search.set('milestone:') + filtered_search.set('milestone') end it 'filters by name' do - send_keys_to_filtered_search('v1') + send_keys_to_filtered_search(':v1') expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name' do - send_keys_to_filtered_search('V1') + send_keys_to_filtered_search(':V1') expect(dropdown_milestone_size).to eq(1) end it 'filters by name with symbol' do - send_keys_to_filtered_search('%v1') + send_keys_to_filtered_search(':%v1') expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name with symbol' do - send_keys_to_filtered_search('%V1') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using double quotes' do - send_keys_to_filtered_search('"future') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using single quotes' do - send_keys_to_filtered_search('\'future p') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using single and double quotes' do - send_keys_to_filtered_search('%"won`\'t f') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using double quotes with symbol' do - send_keys_to_filtered_search('%"future p') - expect(dropdown_milestone_size).to eq(1) - end - - it 'filters by multiple names using single quotes with symbol' do - send_keys_to_filtered_search('%\'future p') + send_keys_to_filtered_search(':%V1') expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters' do - send_keys_to_filtered_search('^+') + send_keys_to_filtered_search(':(+') expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters with symbol' do - send_keys_to_filtered_search('~^+') + send_keys_to_filtered_search(':%(+') expect(dropdown_milestone_size).to eq(1) 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 562673a4ee5..e9841e3c89e 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 @@ -12,8 +12,6 @@ const input = document.createElement('input'); input.classList.add('filtered-search'); document.body.appendChild(input); - - expect(input.value).toBe(''); }); afterEach(() => { From b0d8d742c588baacf0ba08074f23e68498297d10 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 19:52:54 +0800 Subject: [PATCH 166/185] Pass the arguments from where we render the partial Thread: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7345/diffs#note_19707619 --- app/views/projects/issues/index.html.haml | 2 +- app/views/shared/issuable/_search_bar.html.haml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 18e8372ecab..6585e8de1e7 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -32,7 +32,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/search_bar', type: :issues + = render 'shared/issuable/search_bar', type: :issues, finder: issues_finder .issues-holder = render 'issues' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 3449c1f0151..b65d523ddc8 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,3 +1,6 @@ +- type = local_assigns.fetch(:type) +- finder = local_assigns.fetch(:finder) + .issues-filters .issues-details-filters.row-content-block.second-block.filtered-search-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do From 866bb202f29b8f5ad52563dd48ae57168dc6df77 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 19:58:59 +0800 Subject: [PATCH 167/185] Check if it's not NONE too So that we don't have to check it again in somewhere else, and we don't really need to know if it's presented as NONE Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7345/diffs#note_20123999 --- app/finders/issuable_finder.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 2afde8ece65..dfd7de6afa9 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -166,20 +166,20 @@ class IssuableFinder end def assignee_id? - params[:assignee_id].present? + params[:assignee_id].present? && params[:assignee_id] != NONE end def assignee_username? - params[:assignee_username].present? + params[:assignee_username].present? && params[:assignee_username] != NONE end def assignee return @assignee if defined?(@assignee) @assignee = - if assignee_id? && params[:assignee_id] != NONE + if assignee_id? User.find(params[:assignee_id]) - elsif assignee_username? && params[:assignee_username] != NONE + elsif assignee_username? User.find_by(username: params[:assignee_username]) else nil @@ -187,11 +187,11 @@ class IssuableFinder end def author_id? - params[:author_id].present? + params[:author_id].present? && params[:author_id] != NONE end def author_username? - params[:author_username].present? + params[:author_username].present? && params[:author_username] != NONE end def author From 464dddf4d0b54085c10ecb8b62aa7816ed7ba8a3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 20:12:59 +0800 Subject: [PATCH 168/185] Show no issues if author/assignee cannot be found Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7345/diffs#note_19994225 --- app/finders/issuable_finder.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index dfd7de6afa9..dce756544e7 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -178,7 +178,7 @@ class IssuableFinder @assignee = if assignee_id? - User.find(params[:assignee_id]) + User.find_by(id: params[:assignee_id]) elsif assignee_username? User.find_by(username: params[:assignee_username]) else @@ -198,9 +198,9 @@ class IssuableFinder return @author if defined?(@author) @author = - if author_id? && params[:author_id] != NONE - User.find(params[:author_id]) - elsif author_username? && params[:author_username] != NONE + if author_id? + User.find_by(id: params[:author_id]) + elsif author_username? User.find_by(username: params[:author_username]) else nil @@ -275,16 +275,20 @@ class IssuableFinder end def by_assignee(items) - if assignee_id? || assignee_username? - items = items.where(assignee_id: assignee.try(:id)) + if assignee + items = items.where(assignee_id: assignee.id) + elsif assignee_id? || assignee_username? # assignee not found + items = items.none end items end def by_author(items) - if author_id? || author_username? - items = items.where(author_id: author.try(:id)) + if author + items = items.where(author_id: author.id) + elsif author_id? || author_username? # author not found + items = items.none end items From 23d43243a1261be0af6142f7d34063587b11d51b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 20:34:21 +0800 Subject: [PATCH 169/185] Fix various styles for rubocop --- .../features/issues/filtered_search/dropdown_hint_spec.rb | 4 ++-- spec/features/issues/filtered_search/search_bar_spec.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index c4328bc18b0..910abb56569 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -27,7 +27,7 @@ describe 'Dropdown hint', js: true, feature: true do describe 'behavior' do before do expect(page).to have_css(js_dropdown_hint, visible: false) - filtered_search.click() + filtered_search.click end it 'opens when the search bar is first focused' do @@ -35,7 +35,7 @@ describe 'Dropdown hint', js: true, feature: true do end it 'closes when the search bar is unfocused' do - find('body').click() + find('body').click expect(page).to have_css(js_dropdown_hint, visible: false) end end diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index 3af36ae27dd..d5db90ae506 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -53,14 +53,14 @@ describe 'Search bar', js: true, feature: true do end it 'resets the dropdown hint filter' do - filtered_search.click() + filtered_search.click original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size filtered_search.set('author') expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1) find('.filtered-search-input-container .clear-search').click - filtered_search.click() + filtered_search.click expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size) end @@ -73,9 +73,9 @@ describe 'Search bar', js: true, feature: true do expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) find('.filtered-search-input-container .clear-search').click - filtered_search.click() + filtered_search.click expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 - expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq hint_offset + expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset) end end end From f7f9e58092892e5bae0887aa2c0ee9f699085aad Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 16 Dec 2016 22:28:18 +0800 Subject: [PATCH 170/185] Make sure we could query against no one We should separate the idea of not finding anyone, and the idea of against no one. --- app/finders/issuable_finder.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index dce756544e7..5ffaf5ae0f8 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -173,6 +173,10 @@ class IssuableFinder params[:assignee_username].present? && params[:assignee_username] != NONE end + def no_assignee? + params[:assignee_id] == NONE || params[:assignee_username] == NONE + end + def assignee return @assignee if defined?(@assignee) @@ -194,6 +198,10 @@ class IssuableFinder params[:author_username].present? && params[:author_username] != NONE end + def no_author? + params[:author_id] == NONE || params[:author_username] == NONE + end + def author return @author if defined?(@author) @@ -277,6 +285,8 @@ class IssuableFinder def by_assignee(items) if assignee items = items.where(assignee_id: assignee.id) + elsif no_assignee? + items = items.where(assignee_id: nil) elsif assignee_id? || assignee_username? # assignee not found items = items.none end @@ -287,6 +297,8 @@ class IssuableFinder def by_author(items) if author items = items.where(author_id: author.id) + elsif no_author? + items = items.where(author_id: nil) elsif author_id? || author_username? # author not found items = items.none end From 0a1d8875b715431e7631051e29b47c78b6e135ce Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 19 Dec 2016 06:32:42 +0000 Subject: [PATCH 171/185] we're actually not using issue finder here --- app/views/projects/issues/index.html.haml | 2 +- app/views/shared/issuable/_search_bar.html.haml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 6585e8de1e7..18e8372ecab 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -32,7 +32,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/search_bar', type: :issues, finder: issues_finder + = render 'shared/issuable/search_bar', type: :issues .issues-holder = render 'issues' diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index b65d523ddc8..8620deb4dfd 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -1,5 +1,4 @@ - type = local_assigns.fetch(:type) -- finder = local_assigns.fetch(:finder) .issues-filters .issues-details-filters.row-content-block.second-block.filtered-search-block From 896497aba37f9e83585443a108e8611e5bc58488 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 5 Jan 2017 10:57:43 -0600 Subject: [PATCH 172/185] Fix haml lint --- app/views/shared/issuable/_search_bar.html.haml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 8620deb4dfd..33f96a86723 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %i.fa{ class: '{{icon}}'} + %i.fa{ class: "#{'{{icon}}'}" } %span.js-filter-hint {{hint}} %span.js-filter-tag.dropdown-light-content @@ -34,7 +34,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } .dropdown-user-details %span {{name}} @@ -49,7 +49,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user - %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', width: '30' } + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } .dropdown-user-details %span {{name}} @@ -77,7 +77,7 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link - %span.dropdown-label-box{ style: 'background: {{color}}'} + %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} .pull-right @@ -90,9 +90,9 @@ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul %li - %a{href: "#", data: {id: "reopen"}} Open + %a{ href: "#", data: { id: "reopen" } } Open %li - %a{href: "#", data: {id: "close"}} Closed + %a{ href: "#", data: { id: "close" } } Closed .filter-item.inline = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) @@ -104,9 +104,9 @@ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul %li - %a{href: "#", data: {id: "subscribe"}} Subscribe + %a{ href: "#", data: { id: "subscribe" } } Subscribe %li - %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe + %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe = hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag :state_event, params[:state_event] From 8f77b3177f1c342cc0a05c79e45e88bcda04040a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 5 Jan 2017 11:01:56 -0600 Subject: [PATCH 173/185] Add haml lint comment --- app/views/shared/issuable/_search_bar.html.haml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 33f96a86723..8d7b1d616f4 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,6 +25,8 @@ %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } %li.filter-dropdown-item %button.btn.btn-link + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue %i.fa{ class: "#{'{{icon}}'}" } %span.js-filter-hint {{hint}} From c349bb15b628039340054eb132201fdf4a740411 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 5 Jan 2017 13:56:23 -0600 Subject: [PATCH 174/185] Refactor addWordToInput --- .../filtered_search/dropdown_hint.js.es6 | 6 +- .../filtered_search/dropdown_non_user.js.es6 | 4 +- .../filtered_search/dropdown_user.js.es6 | 4 +- .../filtered_search/dropdown_utils.js.es6 | 4 +- .../filtered_search_dropdown.js.es6 | 7 ++- .../filtered_search_dropdown_manager.js.es6 | 37 ++++++------ ...ltered_search_dropdown_manager_spec.js.es6 | 59 ++++++++----------- 7 files changed, 56 insertions(+), 65 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index c5ab9c52d76..f1e317d91cc 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -4,8 +4,8 @@ (() => { class DropdownHint extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); this.config = { droplabFilter: { template: 'hint', @@ -25,7 +25,7 @@ const tag = selected.querySelector('.js-filter-tag').innerText.trim(); if (tag.length) { - gl.FilteredSearchDropdownManager.addWordToInput(token); + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); } this.dismissDropdown(); this.dispatchInputEvent(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 index 54090375c5c..f06c3fc9c6f 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -5,8 +5,8 @@ (() => { class DropdownNonUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, endpoint, symbol) { - super(droplab, dropdown, input); + constructor(droplab, dropdown, input, filter, endpoint, symbol) { + super(droplab, dropdown, input, filter); this.symbol = symbol; this.config = { droplabAjax: { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 49581e3bfbd..e80d266ae89 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -4,8 +4,8 @@ (() => { class DropdownUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input) { - super(droplab, dropdown, input); + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); this.config = { droplabAjaxFilter: { endpoint: '/autocomplete/users.json', diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index 3453311bee5..88b172d6fc4 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -58,11 +58,11 @@ return updatedItem; } - static setDataValueIfSelected(selected) { + static setDataValueIfSelected(filter, selected) { const dataValue = selected.getAttribute('data-value'); if (dataValue) { - gl.FilteredSearchDropdownManager.addWordToInput(dataValue); + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); } // Return boolean based on whether it was set 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 56147ad93c9..886d8113f4a 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -2,10 +2,11 @@ const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; class FilteredSearchDropdown { - constructor(droplab, dropdown, input) { + constructor(droplab, dropdown, input, filter) { this.droplab = droplab; this.hookId = input.getAttribute('data-id'); this.input = input; + this.filter = filter; this.dropdown = dropdown; this.loadingTemplate = `
@@ -30,11 +31,11 @@ const { selected } = e.detail; if (selected.tagName === 'LI' && selected.innerHTML) { - const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(selected); + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); if (!dataValueSet) { const value = getValueFunction(selected); - gl.FilteredSearchDropdownManager.addWordToInput(value); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); } 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 e9f1fbf63ed..8b385d6b642 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 @@ -55,31 +55,30 @@ }; } - static addWordToInput(word, addSpace = false) { + static addWordToInput(tokenName, tokenValue = '') { const input = document.querySelector('.filtered-search'); - input.value = input.value.trim(); + const word = `${tokenName}:${tokenValue}`; - const value = input.value; - const hasExistingValue = value.length !== 0; - const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(value); + 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]; - // Find out what part of the token value the user has typed - // and remove it from input before appending the selected token value - if (lastToken !== searchToken) { - const lastTokenString = `${lastToken.symbol}${lastToken.value}`; + // Remove the typed tokenName + if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') { + // Remove spaces after the colon + if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') { + input.value = input.value.trim(); + } - // Spaces inside the token means that the token value will be escaped by quotes - const hasQuotes = lastTokenString.indexOf(' ') !== -1; - - // Add 2 length to account for the length of the front and back quotes - const lengthToRemove = hasQuotes ? lastTokenString.length + 2 : lastTokenString.length; - input.value = value.slice(0, -1 * (lengthToRemove)); - } else if (searchToken !== '' && word.indexOf(lastSearchToken) !== -1) { - input.value = value.slice(0, -1 * lastSearchToken.length); + input.value = input.value.slice(0, -1 * lastSearchToken.length); + } else if (lastInputCharacter !== ' ') { + // Remove the existing tokenValue + const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`; + input.value = input.value.slice(0, -1 * lastTokenString.length); } - input.value += hasExistingValue && addSpace ? ` ${word}` : word; + input.value += word; } updateCurrentDropdownOffset() { @@ -106,7 +105,7 @@ if (!mappingKey.reference) { const dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput]; + const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); // Passing glArguments to `new gl[glClass]()` 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 e9841e3c89e..d0d27ceb4a6 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 @@ -1,3 +1,4 @@ +//= require extensions/array //= require filtered_search/filtered_search_tokenizer //= require filtered_search/filtered_search_dropdown_manager @@ -8,6 +9,10 @@ return document.querySelector('.filtered-search').value; } + function setInputValue(value) { + document.querySelector('.filtered-search').value = value; + } + beforeEach(() => { const input = document.createElement('input'); input.classList.add('filtered-search'); @@ -19,47 +24,33 @@ }); describe('input has no existing value', () => { - it('should add word', () => { - gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInputValue()).toBe('firstWord'); + it('should add just tokenName', () => { + gl.FilteredSearchDropdownManager.addWordToInput('milestone'); + expect(getInputValue()).toBe('milestone:'); }); - it('should not add space before first word', () => { - gl.FilteredSearchDropdownManager.addWordToInput('firstWord', true); - expect(getInputValue()).toBe('firstWord'); - }); - - it('should not add space before second word by default', () => { - gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInputValue()).toBe('firstWord'); - gl.FilteredSearchDropdownManager.addWordToInput('secondWord'); - expect(getInputValue()).toBe('firstWordsecondWord'); - }); - - it('should add space before new word when addSpace is passed', () => { - expect(getInputValue()).toBe(''); - gl.FilteredSearchDropdownManager.addWordToInput('firstWord'); - expect(getInputValue()).toBe('firstWord'); - gl.FilteredSearchDropdownManager.addWordToInput('secondWord', true); - expect(getInputValue()).toBe('firstWord secondWord'); + it('should add tokenName and tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); + expect(getInputValue()).toBe('label:none'); }); }); - describe('input has exsting value', () => { - it('should only add the remaining characters of the word', () => { - const lastToken = { - key: 'author', - value: 'roo', - }; - - document.querySelector('.filtered-search').value = `${lastToken.key}:${lastToken.value}`; - gl.FilteredSearchDropdownManager.addWordToInput('root'); - expect(getInputValue()).toBe('author:root'); + describe('input has existing value', () => { + it('should be able to just add tokenName', () => { + setInputValue('a'); + gl.FilteredSearchDropdownManager.addWordToInput('author'); + expect(getInputValue()).toBe('author:'); }); - it('should only add the remaining characters of the word (contains space)', () => { - document.querySelector('.filtered-search').value = 'label:~"test'; - gl.FilteredSearchDropdownManager.addWordToInput('~\'"test me"\''); + it('should replace tokenValue', () => { + setInputValue('author:roo'); + gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); + expect(getInputValue()).toBe('author:@root'); + }); + + it('should add tokenValues containing spaces', () => { + setInputValue('label:~"test'); + gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); expect(getInputValue()).toBe('label:~\'"test me"\''); }); }); From d5dee97becf193627f407815aa4013ea3c0a47a2 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 6 Jan 2017 12:47:00 -0600 Subject: [PATCH 175/185] Enable filtering with multiple words --- .../javascripts/filtered_search/dropdown_utils.js.es6 | 6 +++++- .../filtered_search/filtered_search_dropdown_manager.js.es6 | 2 +- .../filtered_search/filtered_search_tokenizer.js.es6 | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 index 88b172d6fc4..c27ef3042d1 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -25,8 +25,12 @@ const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); if (lastToken !== searchToken) { - const value = lastToken.value.toLowerCase(); const title = updatedItem.title.toLowerCase(); + let value = lastToken.value.toLowerCase(); + + if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + value = value.slice(1); + } // Eg. filterSymbol = ~ for labels const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; 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 8b385d6b642..1cd0483877a 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 @@ -72,7 +72,7 @@ } input.value = input.value.slice(0, -1 * lastSearchToken.length); - } else if (lastInputCharacter !== ' ') { + } 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); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index 14ca78e139b..60473dddead 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -1,7 +1,7 @@ (() => { class FilteredSearchTokenizer { static processTokens(input) { - const tokenRegex = /(\w+):([~%@]?)(?:"(.*?)"|'(.*?)'|(\S+))/g; + const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; const tokens = []; let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { From 0f973c28b7f4852119181b549255308c76924c4e Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 6 Jan 2017 13:56:34 -0600 Subject: [PATCH 176/185] Fix specs --- .../filtered_search/filtered_search_manager.js.es6 | 9 ++++++++- .../filtered_search/dropdown_utils_spec.js.es6 | 7 ++++--- .../filtered_search_tokenizer_spec.js.es6 | 4 ++-- 3 files changed, 14 insertions(+), 6 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 d2ea4de18aa..bd3c4240f13 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -142,7 +142,14 @@ if (condition) { tokenPath = condition.url; } else { - tokenPath = `${keyParam}=${encodeURIComponent(token.value)}`; + let tokenValue = token.value; + + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + tokenValue = tokenValue.slice(1, tokenValue.length - 1); + } + + tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; } paths.push(tokenPath); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 index 369eb90e31c..ce61b73aa8a 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -1,3 +1,4 @@ +//= require extensions/array //= require filtered_search/dropdown_utils //= require filtered_search/filtered_search_tokenizer //= require filtered_search/filtered_search_dropdown_manager @@ -80,7 +81,7 @@ getAttribute: () => 'value', }; - gl.DropdownUtils.setDataValueIfSelected(selected); + gl.DropdownUtils.setDataValueIfSelected(null, selected); expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); }); @@ -89,7 +90,7 @@ getAttribute: () => 'value', }; - const result = gl.DropdownUtils.setDataValueIfSelected(selected); + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); expect(result).toBe(true); }); @@ -98,7 +99,7 @@ getAttribute: () => null, }; - const result = gl.DropdownUtils.setDataValueIfSelected(selected); + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); expect(result).toBe(false); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index 8e5992fa446..ac7f8e9cbcd 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -24,7 +24,7 @@ expect(results.tokens[0].symbol).toBe('@'); expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('Very Important'); + expect(results.tokens[1].value).toBe('"Very Important"'); expect(results.tokens[1].symbol).toBe('~'); expect(results.tokens[2].key).toBe('milestone'); @@ -72,7 +72,7 @@ expect(results.tokens[0].symbol).toBe('@'); expect(results.tokens[1].key).toBe('label'); - expect(results.tokens[1].value).toBe('Won\'t fix'); + expect(results.tokens[1].value).toBe('"Won\'t fix"'); expect(results.tokens[1].symbol).toBe('~'); expect(results.tokens[2].key).toBe('milestone'); From 3d0b0a62609d5f961d2777f497558d90c1dc039b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 6 Jan 2017 14:05:53 -0600 Subject: [PATCH 177/185] Add symbols --- .../javascripts/filtered_search/dropdown_hint.js.es6 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 index f1e317d91cc..63c20f57520 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -37,19 +37,19 @@ const dropdownData = [{ icon: 'fa-pencil', hint: 'author:', - tag: '<author>', + tag: '<@author>', }, { icon: 'fa-user', hint: 'assignee:', - tag: '<assignee>', + tag: '<@assignee>', }, { icon: 'fa-clock-o', hint: 'milestone:', - tag: '<milestone>', + tag: '<%milestone>', }, { icon: 'fa-tag', hint: 'label:', - tag: '<label>', + tag: '<~label>', }]; this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); From 1bfdad5c4707c9dac243cebaf4666fee77f17891 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 11:33:17 -0600 Subject: [PATCH 178/185] Code review changes --- app/assets/javascripts/dispatcher.js.es6 | 2 +- app/assets/javascripts/droplab/droplab_ajax.js | 2 +- .../filtered_search/filtered_search_tokenizer.js.es6 | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index ca2da18dc26..99a34651639 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -84,7 +84,7 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': - if (document.querySelector('.filtered-search') && gl.FilteredSearchManager) { + if (gl.FilteredSearchManager) { new gl.FilteredSearchManager(); } Issuable.init(); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index ebb518eeef4..926e53e696f 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -39,7 +39,7 @@ require('../window')(function(w){ var loadingTemplate = document.createElement('div'); loadingTemplate.innerHTML = config.loadingTemplate; - loadingTemplate.setAttribute('data-loading-template', true); + loadingTemplate.setAttribute('data-loading-template', ''); this.listTemplate = dynamicList.outerHTML; dynamicList.outerHTML = loadingTemplate.outerHTML; diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 index 60473dddead..cf53845a48b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -1,6 +1,8 @@ (() => { class FilteredSearchTokenizer { static processTokens(input) { + // Regex extracts `(token):(symbol)(value)` + // Values that start with a double quote must end in a double quote (same for single) const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; const tokens = []; let lastToken = null; From 4ec8eb9abefb1a2abed9b5cbee4292325b3c22f6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 12:15:10 -0600 Subject: [PATCH 179/185] Fix javascript error for when there are no issues --- .../filtered_search_manager.js.es6 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 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 bd3c4240f13..ffd0d7e9cba 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -3,17 +3,20 @@ (() => { class FilteredSearchManager { constructor() { - this.tokenizer = gl.FilteredSearchTokenizer; this.filteredSearchInput = document.querySelector('.filtered-search'); this.clearSearchButton = document.querySelector('.clear-search'); - this.dropdownManager = new gl.FilteredSearchDropdownManager(); - this.bindEvents(); - this.loadSearchParamsFromURL(); - this.dropdownManager.setDropdown(); + if (this.filteredSearchInput) { + this.tokenizer = gl.FilteredSearchTokenizer; + this.dropdownManager = new gl.FilteredSearchDropdownManager(); - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('page:fetch', this.cleanupWrapper); + this.bindEvents(); + this.loadSearchParamsFromURL(); + this.dropdownManager.setDropdown(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } } cleanup() { From c0287e69c6717fe9de5ff0b804f952410e453ef8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 15:43:17 -0600 Subject: [PATCH 180/185] Fix indentation --- .../javascripts/droplab/droplab_ajax.js | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index 926e53e696f..c8850f121d7 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -45,22 +45,23 @@ require('../window')(function(w){ dynamicList.outerHTML = loadingTemplate.outerHTML; } - this._loadUrlData(config.endpoint).then(function(d) { - if (config.loadingTemplate) { - var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]'); + this._loadUrlData(config.endpoint) + .then(function(d) { + if (config.loadingTemplate) { + var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]'); - if (dataLoadingTemplate) { - dataLoadingTemplate.outerHTML = self.listTemplate; + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } } - } - hook.list[config.method].call(hook.list, d); - }).catch(function(e) { - if(e.message) { - console.error(e.message, e.stack); // eslint-disable-line no-console - } else { - console.error(e); // eslint-disable-line no-console - } - }); + hook.list[config.method].call(hook.list, d); + }).catch(function(e) { + if(e.message) { + console.error(e.message, e.stack); // eslint-disable-line no-console + } else { + console.error(e); // eslint-disable-line no-console + } + }); }, destroy: function() { From e0e855b5f49bc8efc3ca69aa83ea28d6becb53cc Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 16:23:51 -0600 Subject: [PATCH 181/185] Fix code review suggestions --- app/assets/javascripts/droplab/droplab_ajax.js | 10 +++++----- app/assets/javascripts/lib/utils/common_utils.js.es6 | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js index c8850f121d7..f20610b3811 100644 --- a/app/assets/javascripts/droplab/droplab_ajax.js +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -3,6 +3,10 @@ /* global droplab */ require('../window')(function(w){ + function droplabAjaxException(message) { + this.message = message; + } + w.droplabAjax = { _loadUrlData: function _loadUrlData(url) { return new Promise(function(resolve, reject) { @@ -56,11 +60,7 @@ require('../window')(function(w){ } hook.list[config.method].call(hook.list, d); }).catch(function(e) { - if(e.message) { - console.error(e.message, e.stack); // eslint-disable-line no-console - } else { - console.error(e); // eslint-disable-line no-console - } + throw new droplabAjaxException(e.message || e); }); }, diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 8aa78f407e5..3e2c75d3cc6 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -169,6 +169,8 @@ w.gl.utils.getParameterByName = (name) => { const url = window.location.href; name = name.replace(/[[\]]/g, '\\$&'); + // Finds the value associated to the name + // Example, state=open where state is the name and open is the value const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); const results = regex.exec(url); if (!results) return null; From 757bc8ecf43159243d9a0f45f27844f6572e60ac Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 9 Jan 2017 17:41:53 -0600 Subject: [PATCH 182/185] Remove duplicate method --- .../javascripts/lib/utils/common_utils.js.es6 | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 3e2c75d3cc6..0c6a3cc3170 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -130,22 +130,6 @@ return window.location.search.slice(1).split('&'); }; - gl.utils.getParameterByName = function(name) { - var url = window.location.href; - var param = name.replace(/[[\]]/g, '\\$&'); - var regex = new RegExp('[?&]' + param + '(=([^&#]*)|&|#|$)'); - var results = regex.exec(url); - - if (!results) { - return null; - } - - if (!results[2]) { - return ''; - } - return decodeURIComponent(results[2].replace(/\+/g, ' ')); - }; - gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; @@ -169,8 +153,6 @@ w.gl.utils.getParameterByName = (name) => { const url = window.location.href; name = name.replace(/[[\]]/g, '\\$&'); - // Finds the value associated to the name - // Example, state=open where state is the name and open is the value const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); const results = regex.exec(url); if (!results) return null; From 044a195b1e8ca854e67f8e2782bc69c345bf0df6 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 10 Jan 2017 20:51:57 -0500 Subject: [PATCH 183/185] Add comments to issuable finder --- app/finders/issuable_finder.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 5ffaf5ae0f8..1576fc80a6b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -174,6 +174,7 @@ class IssuableFinder end def no_assignee? + # Assignee_id takes precedence over assignee_username params[:assignee_id] == NONE || params[:assignee_username] == NONE end @@ -199,6 +200,7 @@ class IssuableFinder end def no_author? + # author_id takes precedence over author_username params[:author_id] == NONE || params[:author_username] == NONE end From ce9d3ee599f816ba20d57eae16b4139cabecdb1a Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 11 Jan 2017 20:47:42 -0500 Subject: [PATCH 184/185] Backend review --- .../droplab/droplab_ajax_filter.js | 9 +- .../filtered_search/dropdown_assignee_spec.rb | 18 +++ .../filtered_search/dropdown_author_spec.rb | 16 +++ .../filtered_search/dropdown_hint_spec.rb | 11 ++ .../filtered_search/dropdown_label_spec.rb | 25 +++++ .../dropdown_milestone_spec.rb | 25 +++++ .../filtered_search/filter_issues_spec.rb | 103 +++++++++++++++--- .../issues/filtered_search/search_bar_spec.rb | 7 ++ 8 files changed, 195 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js index ae316f881c8..af163f76851 100644 --- a/app/assets/javascripts/droplab/droplab_ajax_filter.js +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -22,7 +22,8 @@ require('../window')(function(w){ debounceTrigger: function debounceTrigger(e) { var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93]; var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1; - var focusEvent = false; + var focusEvent = e.type === 'focus'; + if (invalidKeyPressed || this.loading) { return; } @@ -31,10 +32,6 @@ require('../window')(function(w){ clearTimeout(this.timeout); } - if (e.type === 'focus') { - focusEvent = true; - } - this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200); }, @@ -66,7 +63,7 @@ require('../window')(function(w){ searchValue = ''; } - if (searchValue === config.searchKey) { + if (config.searchKey === searchValue) { return this.list.show(); } diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index a5a05a622b6..6f6a2532c04 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -39,26 +39,31 @@ describe 'Dropdown assignee', js: true, feature: true do describe 'behavior' do it 'opens when the search bar has assignee:' do filtered_search.set('assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'closes when the search bar is unfocused' do find('body').click() + expect(page).to have_css(js_dropdown_assignee, visible: false) end it 'should show loading indicator when opened' do filtered_search.set('assignee:') + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) end it 'should hide loading indicator when loaded' do send_keys_to_filtered_search('assignee:') + expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading') end it 'should load all the assignees when opened' do send_keys_to_filtered_search('assignee:') + expect(dropdown_assignee_size).to eq(3) end end @@ -70,31 +75,37 @@ describe 'Dropdown assignee', js: true, feature: true do it 'filters by name' do send_keys_to_filtered_search('j') + expect(dropdown_assignee_size).to eq(2) end it 'filters by case insensitive name' do send_keys_to_filtered_search('J') + expect(dropdown_assignee_size).to eq(2) end it 'filters by username with symbol' do send_keys_to_filtered_search('@ot') + expect(dropdown_assignee_size).to eq(2) end it 'filters by case insensitive username with symbol' do send_keys_to_filtered_search('@OT') + expect(dropdown_assignee_size).to eq(2) end it 'filters by username without symbol' do send_keys_to_filtered_search('ot') + expect(dropdown_assignee_size).to eq(2) end it 'filters by case insensitive username without symbol' do send_keys_to_filtered_search('OT') + expect(dropdown_assignee_size).to eq(2) end end @@ -106,6 +117,7 @@ describe 'Dropdown assignee', js: true, feature: true do it 'fills in the assignee username when the assignee has not been filtered' do click_assignee(user_jacob.name) + expect(page).to have_css(js_dropdown_assignee, visible: false) expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") end @@ -113,12 +125,14 @@ describe 'Dropdown assignee', js: true, feature: true do it 'fills in the assignee username when the assignee has been filtered' do send_keys_to_filtered_search('roo') click_assignee(user.name) + expect(page).to have_css(js_dropdown_assignee, visible: false) expect(filtered_search.value).to eq("assignee:@#{user.username}") end it 'selects `no assignee`' do find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click + expect(page).to have_css(js_dropdown_assignee, visible: false) expect(filtered_search.value).to eq("assignee:none") end @@ -127,21 +141,25 @@ describe 'Dropdown assignee', js: true, feature: true do describe 'input has existing content' do it 'opens assignee dropdown with existing search term' do filtered_search.set('searchTerm assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing author' do filtered_search.set('author:@user assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing label' do filtered_search.set('label:~bug assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing milestone' do filtered_search.set('milestone:%v1.0 assignee:') + expect(page).to have_css(js_dropdown_assignee, visible: true) end end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index d053b032768..60a86cc93d4 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -39,26 +39,31 @@ describe 'Dropdown author', js: true, feature: true do describe 'behavior' do it 'opens when the search bar has author:' do filtered_search.set('author:') + expect(page).to have_css(js_dropdown_author, visible: true) end it 'closes when the search bar is unfocused' do find('body').click() + expect(page).to have_css(js_dropdown_author, visible: false) end it 'should show loading indicator when opened' do filtered_search.set('author:') + expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true) end it 'should hide loading indicator when loaded' do send_keys_to_filtered_search('author:') + expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading') end it 'should load all the authors when opened' do send_keys_to_filtered_search('author:') + expect(dropdown_author_size).to eq(3) end end @@ -71,26 +76,31 @@ describe 'Dropdown author', js: true, feature: true do it 'filters by name' do send_keys_to_filtered_search('ja') + expect(dropdown_author_size).to eq(1) end it 'filters by case insensitive name' do send_keys_to_filtered_search('Ja') + expect(dropdown_author_size).to eq(1) end it 'filters by username with symbol' do send_keys_to_filtered_search('@ot') + expect(dropdown_author_size).to eq(2) end it 'filters by username without symbol' do send_keys_to_filtered_search('ot') + expect(dropdown_author_size).to eq(2) end it 'filters by case insensitive username without symbol' do send_keys_to_filtered_search('OT') + expect(dropdown_author_size).to eq(2) end end @@ -103,12 +113,14 @@ describe 'Dropdown author', js: true, feature: true do it 'fills in the author username when the author has not been filtered' do click_author(user_jacob.name) + expect(page).to have_css(js_dropdown_author, visible: false) expect(filtered_search.value).to eq("author:@#{user_jacob.username}") end it 'fills in the author username when the author has been filtered' do click_author(user.name) + expect(page).to have_css(js_dropdown_author, visible: false) expect(filtered_search.value).to eq("author:@#{user.username}") end @@ -117,21 +129,25 @@ describe 'Dropdown author', js: true, feature: true do describe 'input has existing content' do it 'opens author dropdown with existing search term' do filtered_search.set('searchTerm author:') + expect(page).to have_css(js_dropdown_author, visible: true) end it 'opens author dropdown with existing assignee' do filtered_search.set('assignee:@user author:') + expect(page).to have_css(js_dropdown_author, visible: true) end it 'opens author dropdown with existing label' do filtered_search.set('label:~bug author:') + expect(page).to have_css(js_dropdown_author, visible: true) end it 'opens author dropdown with existing milestone' do filtered_search.set('milestone:%v1.0 author:') + expect(page).to have_css(js_dropdown_author, visible: true) end end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb index 910abb56569..04dd54ab459 100644 --- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -36,6 +36,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'closes when the search bar is unfocused' do find('body').click + expect(page).to have_css(js_dropdown_hint, visible: false) end end @@ -43,12 +44,14 @@ describe 'Dropdown hint', js: true, feature: true do describe 'filtering' do it 'does not filter `Keep typing and press Enter`' do filtered_search.set('randomtext') + expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false) expect(dropdown_hint_size).to eq(0) end it 'filters with text' do filtered_search.set('a') + expect(dropdown_hint_size).to eq(3) end end @@ -60,6 +63,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the author dropdown when you click on author' do click_hint('author') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) expect(filtered_search.value).to eq('author:') @@ -67,6 +71,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the assignee dropdown when you click on assignee' do click_hint('assignee') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(filtered_search.value).to eq('assignee:') @@ -74,6 +79,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the milestone dropdown when you click on milestone' do click_hint('milestone') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(filtered_search.value).to eq('milestone:') @@ -81,6 +87,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the label dropdown when you click on label' do click_hint('label') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) expect(filtered_search.value).to eq('label:') @@ -91,6 +98,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the author dropdown when you click on author' do filtered_search.set('auth') click_hint('author') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-author', visible: true) expect(filtered_search.value).to eq('author:') @@ -99,6 +107,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the assignee dropdown when you click on assignee' do filtered_search.set('assign') click_hint('assignee') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-assignee', visible: true) expect(filtered_search.value).to eq('assignee:') @@ -107,6 +116,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the milestone dropdown when you click on milestone' do filtered_search.set('mile') click_hint('milestone') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-milestone', visible: true) expect(filtered_search.value).to eq('milestone:') @@ -115,6 +125,7 @@ describe 'Dropdown hint', js: true, feature: true do it 'opens the label dropdown when you click on label' do filtered_search.set('lab') click_hint('label') + expect(page).to have_css(js_dropdown_hint, visible: false) expect(page).to have_css('#js-dropdown-label', visible: true) expect(filtered_search.value).to eq('label:') diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index 1a21b0d64ed..89c144141c9 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -43,26 +43,31 @@ describe 'Dropdown label', js: true, feature: true do describe 'behavior' do it 'opens when the search bar has label:' do filtered_search.set('label:') + expect(page).to have_css(js_dropdown_label, visible: true) end it 'closes when the search bar is unfocused' do find('body').click() + expect(page).to have_css(js_dropdown_label, visible: false) end it 'should show loading indicator when opened' do filtered_search.set('label:') + expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true) end it 'should hide loading indicator when loaded' do send_keys_to_filtered_search('label:') + expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading') end it 'should load all the labels when opened' do send_keys_to_filtered_search('label:') + expect(dropdown_label_size).to be > 0 end end @@ -74,61 +79,73 @@ describe 'Dropdown label', js: true, feature: true do it 'filters by name' do send_keys_to_filtered_search(':b') + expect(dropdown_label_size).to eq(2) end it 'filters by case insensitive name' do send_keys_to_filtered_search(':B') + expect(dropdown_label_size).to eq(2) end it 'filters by name with symbol' do send_keys_to_filtered_search(':~bu') + expect(dropdown_label_size).to eq(2) end it 'filters by case insensitive name with symbol' do send_keys_to_filtered_search(':~BU') + expect(dropdown_label_size).to eq(2) end it 'filters by multiple words' do send_keys_to_filtered_search(':Hig') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words with symbol' do send_keys_to_filtered_search(':~Hig') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words containing single quotes' do send_keys_to_filtered_search(':won\'t') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words containing single quotes with symbol' do send_keys_to_filtered_search(':~won\'t') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words containing double quotes' do send_keys_to_filtered_search(':won"t') + expect(dropdown_label_size).to eq(1) end it 'filters by multiple words containing double quotes with symbol' do send_keys_to_filtered_search(':~won"t') + expect(dropdown_label_size).to eq(1) end it 'filters by special characters' do send_keys_to_filtered_search(':^+') + expect(dropdown_label_size).to eq(1) end it 'filters by special characters with symbol' do send_keys_to_filtered_search(':~^+') + expect(dropdown_label_size).to eq(1) end end @@ -140,6 +157,7 @@ describe 'Dropdown label', js: true, feature: true do it 'fills in the label name when the label has not been filled' do click_label(bug_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~#{bug_label.title}") end @@ -147,42 +165,49 @@ describe 'Dropdown label', js: true, feature: true do it 'fills in the label name when the label is partially filled' do send_keys_to_filtered_search('bu') click_label(bug_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~#{bug_label.title}") end it 'fills in the label name that contains multiple words' do click_label(two_words_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") end it 'fills in the label name that contains multiple words and is very long' do click_label(long_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") end it 'fills in the label name that contains double quotes' do click_label(wont_fix_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") end it 'fills in the label name with the correct capitalization' do click_label(uppercase_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") end it 'fills in the label name with special characters' do click_label(special_label.title) + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:~#{special_label.title}") end it 'selects `no label`' do find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click + expect(page).to have_css(js_dropdown_label, visible: false) expect(filtered_search.value).to eq("label:none") end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index 64fc83c6ccb..e5a271b663f 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -47,26 +47,31 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'behavior' do it 'opens when the search bar has milestone:' do filtered_search.set('milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'closes when the search bar is unfocused' do find('body').click() + expect(page).to have_css(js_dropdown_milestone, visible: false) end it 'should show loading indicator when opened' do filtered_search.set('milestone:') + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true) end it 'should hide loading indicator when loaded' do send_keys_to_filtered_search('milestone:') + expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading') end it 'should load all the milestones when opened' do send_keys_to_filtered_search('milestone:') + expect(dropdown_milestone_size).to be > 0 end end @@ -78,31 +83,37 @@ describe 'Dropdown milestone', js: true, feature: true do it 'filters by name' do send_keys_to_filtered_search(':v1') + expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name' do send_keys_to_filtered_search(':V1') + expect(dropdown_milestone_size).to eq(1) end it 'filters by name with symbol' do send_keys_to_filtered_search(':%v1') + expect(dropdown_milestone_size).to eq(1) end it 'filters by case insensitive name with symbol' do send_keys_to_filtered_search(':%V1') + expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters' do send_keys_to_filtered_search(':(+') + expect(dropdown_milestone_size).to eq(1) end it 'filters by special characters with symbol' do send_keys_to_filtered_search(':%(+') + expect(dropdown_milestone_size).to eq(1) end end @@ -114,6 +125,7 @@ describe 'Dropdown milestone', js: true, feature: true do it 'fills in the milestone name when the milestone has not been filled' do click_milestone(milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%#{milestone.title}") end @@ -121,48 +133,56 @@ describe 'Dropdown milestone', js: true, feature: true do it 'fills in the milestone name when the milestone is partially filled' do send_keys_to_filtered_search('v') click_milestone(milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%#{milestone.title}") end it 'fills in the milestone name that contains multiple words' do click_milestone(two_words_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") end it 'fills in the milestone name that contains multiple words and is very long' do click_milestone(long_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") end it 'fills in the milestone name that contains double quotes' do click_milestone(wont_fix_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") end it 'fills in the milestone name with the correct capitalization' do click_milestone(uppercase_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") end it 'fills in the milestone name with special characters' do click_milestone(special_milestone.title) + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") end it 'selects `no milestone`' do click_static_milestone('No Milestone') + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:none") end it 'selects `upcoming milestone`' do click_static_milestone('Upcoming') + expect(page).to have_css(js_dropdown_milestone, visible: false) expect(filtered_search.value).to eq("milestone:upcoming") end @@ -171,26 +191,31 @@ describe 'Dropdown milestone', js: true, feature: true do describe 'input has existing content' do it 'opens milestone dropdown with existing search term' do filtered_search.set('searchTerm milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'opens milestone dropdown with existing author' do filtered_search.set('author:@john milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'opens milestone dropdown with existing assignee' do filtered_search.set('assignee:@john milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'opens milestone dropdown with existing label' do filtered_search.set('label:~important milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end it 'opens milestone dropdown with existing milestone' do filtered_search.set('milestone:%100 milestone:') + expect(page).to have_css(js_dropdown_milestone, visible: true) end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index ed6d4a0787a..7cae7152aa2 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -101,15 +101,18 @@ describe 'Filter issues', js: true, feature: true do context 'only author' do it 'filters issues by searched author' do input_filtered_search("author:@#{user.username}") + expect_issues_list_count(5) end it 'filters issues by invalid author' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by multiple authors' do - # YOLO + pending('to be tested, issue #26546') + fail end end @@ -117,6 +120,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author and text' do search = "author:@#{user.username} issue" input_filtered_search(search) + expect_issues_list_count(3) expect_filtered_search_input(search) end @@ -124,6 +128,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author, assignee and text' do search = "author:@#{user.username} assignee:@#{user.username} issue" input_filtered_search(search) + expect_issues_list_count(3) expect_filtered_search_input(search) end @@ -131,6 +136,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author, assignee, label, and text' do search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -138,13 +144,15 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched author, assignee, label, milestone and text' do search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end end - context 'sorting' do - # TODO + it 'sorting' do + pending('to be tested, issue #26546') + fail end end @@ -153,6 +161,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee' do search = "assignee:@#{user.username}" input_filtered_search(search) + expect_issues_list_count(5) expect_filtered_search_input(search) end @@ -160,16 +169,19 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by no assignee' do search = "assignee:none" input_filtered_search(search) + expect_issues_list_count(8, 1) expect_filtered_search_input(search) end it 'filters issues by invalid assignee' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by multiple assignees' do - # YOLO + pending('to be tested, issue #26546') + fail end end @@ -177,6 +189,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee and text' do search = "assignee:@#{user.username} searchTerm" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -184,6 +197,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee, author and text' do search = "assignee:@#{user.username} author:@#{user.username} searchTerm" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -191,6 +205,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee, author, label, text' do search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -198,13 +213,17 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched assignee, author, label, milestone and text' do search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end end context 'sorting' do - # TODO + it 'sorts' do + pending('to be tested, issue #26546') + fail + end end end @@ -213,6 +232,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label' do search = "label:~#{bug_label.title}" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -220,17 +240,20 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by no label' do search = "label:none" input_filtered_search(search) + expect_issues_list_count(9, 1) expect_filtered_search_input(search) end it 'filters issues by invalid label' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by multiple labels' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -242,6 +265,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~#{special_label.title}" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -251,6 +275,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~#{new_label.title}" input_filtered_search(search) + expect_no_issues_list() expect_filtered_search_input(search) end @@ -264,6 +289,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~'#{special_multiple_label.title}'" input_filtered_search(search) + expect_issues_list_count(1) # filtered search defaults quotations to double quotes @@ -273,14 +299,15 @@ describe 'Filter issues', js: true, feature: true do it 'single quotes' do search = "label:~'#{multiple_words_label.title}'" input_filtered_search(search) - expect_issues_list_count(1) + expect_issues_list_count(1) expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") end it 'double quotes' do search = "label:~\"#{multiple_words_label.title}\"" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -292,6 +319,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~'#{double_quotes_label.title}'" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -303,6 +331,7 @@ describe 'Filter issues', js: true, feature: true do search = "label:~\"#{single_quotes_label.title}\"" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -312,6 +341,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label and text' do search = "label:~#{caps_sensitive_label.title} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -319,6 +349,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, author and text' do search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -326,6 +357,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, author, assignee and text' do search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -333,6 +365,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, author, assignee, milestone and text' do search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -342,6 +375,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, label2, and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -349,6 +383,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, label2, author and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -356,6 +391,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, label2, author, assignee and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -363,6 +399,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched label, label2, author, assignee, milestone and text' do search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -384,7 +421,10 @@ describe 'Filter issues', js: true, feature: true do end context 'sorting' do - # TODO + it 'sorts' do + pending('to be tested, issue #26546') + fail + end end end @@ -392,25 +432,30 @@ describe 'Filter issues', js: true, feature: true do context 'only milestone' do it 'filters issues by searched milestone' do input_filtered_search("milestone:%#{milestone.title}") + expect_issues_list_count(5) end it 'filters issues by no milestone' do input_filtered_search("milestone:none") + expect_issues_list_count(7, 1) end it 'filters issues by upcoming milestones' do input_filtered_search("milestone:upcoming") + expect_issues_list_count(1) end it 'filters issues by invalid milestones' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by multiple milestones' do - # YOLO + pending('to be tested, issue #26546') + fail end it 'filters issues by milestone containing special characters' do @@ -419,6 +464,7 @@ describe 'Filter issues', js: true, feature: true do search = "milestone:%#{special_milestone.title}" input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -428,6 +474,7 @@ describe 'Filter issues', js: true, feature: true do search = "milestone:%#{new_milestone.title}" input_filtered_search(search) + expect_no_issues_list() expect_filtered_search_input(search) end @@ -437,6 +484,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone and text' do search = "milestone:%#{milestone.title} bug" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -444,6 +492,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone, author and text' do search = "milestone:%#{milestone.title} author:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -451,6 +500,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone, author, assignee and text' do search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end @@ -458,13 +508,17 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched milestone, author, assignee, label and text' do search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" input_filtered_search(search) + expect_issues_list_count(2) expect_filtered_search_input(search) end end context 'sorting' do - # TODO + it 'sorts' do + pending('to be tested, issue #26546') + fail + end end end @@ -473,6 +527,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched text' do search = 'Bug' input_filtered_search(search) + expect_issues_list_count(4, 1) expect_filtered_search_input(search) end @@ -480,6 +535,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by multiple searched text' do search = 'Bug report' input_filtered_search(search) + expect_issues_list_count(3) expect_filtered_search_input(search) end @@ -487,6 +543,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by case insensitive searched text' do search = 'bug report' input_filtered_search(search) + expect_issues_list_count(3) expect_filtered_search_input(search) end @@ -494,6 +551,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched text containing single quotes' do search = '\'single quotes\'' input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -501,6 +559,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched text containing double quotes' do search = '"double quotes"' input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -508,6 +567,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by searched text containing special characters' do search = '!@#{$%^&*()-+' input_filtered_search(search) + expect_issues_list_count(1) expect_filtered_search_input(search) end @@ -515,6 +575,7 @@ describe 'Filter issues', js: true, feature: true do it 'does not show any issues' do search = 'testing' input_filtered_search(search) + expect_no_issues_list() expect_filtered_search_input(search) end @@ -523,66 +584,77 @@ describe 'Filter issues', js: true, feature: true do context 'searched text with other filters' do it 'filters issues by searched text and author' do input_filtered_search("bug author:@#{user.username}") + expect_issues_list_count(2) expect_filtered_search_input("author:@#{user.username} bug") end it 'filters issues by searched text, author and more text' do input_filtered_search("bug author:@#{user.username} report") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} bug report") end it 'filters issues by searched text, author and assignee' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") + expect_issues_list_count(2) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") end it 'filters issues by searched text, author, more text and assignee' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") end it 'filters issues by searched text, author, more text, assignee and even more text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") end it 'filters issues by searched text, author, assignee and label' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") + expect_issues_list_count(2) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") end it 'filters issues by searched text, author, assignee, label and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") + expect_issues_list_count(2) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") end it 'filters issues by searched text, author, assignee, multiple labels and milestone' do input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") end it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") + expect_issues_list_count(1) expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") end @@ -605,6 +677,7 @@ describe 'Filter issues', js: true, feature: true do updated_at: 5.days.ago) input_filtered_search('days ago') + expect_issues_list_count(2) sort_toggle = find('.filtered-search-container .dropdown-toggle') @@ -639,6 +712,7 @@ describe 'Filter issues', js: true, feature: true do it 'closed state' do find('.issues-state-filters a', text: 'Closed').click wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 1) expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) end @@ -646,6 +720,7 @@ describe 'Filter issues', js: true, feature: true do it 'all state' do find('.issues-state-filters a', text: 'All').click wait_for_ajax + expect(page).to have_selector('.issues-list .issue', count: 5) end end @@ -657,6 +732,7 @@ describe 'Filter issues', js: true, feature: true do params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) @@ -671,6 +747,7 @@ describe 'Filter issues', js: true, feature: true do params = CGI.parse(URI.parse(link[:href]).query) auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + expect(params).to include('private_token' => [user.private_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index d5db90ae506..56b1d354eb0 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -27,6 +27,7 @@ describe 'Search bar', js: true, feature: true do expect(filtered_search.value).to eq(search_text) find('.filtered-search-input-container .clear-search').click + expect(filtered_search.value).to eq('') end @@ -37,12 +38,14 @@ describe 'Search bar', js: true, feature: true do it 'hides after clicked' do filtered_search.set('a') find('.filtered-search-input-container .clear-search').click + expect(page).to have_css('.clear-search', visible: false) end it 'hides when there is no text' do filtered_search.set('a') filtered_search.set('') + expect(page).to have_css('.clear-search', visible: false) end @@ -57,10 +60,12 @@ describe 'Search bar', js: true, feature: true do original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size filtered_search.set('author') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1) find('.filtered-search-input-container .clear-search').click filtered_search.click + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size) end @@ -70,10 +75,12 @@ describe 'Search bar', js: true, feature: true do hint_offset = get_left_style(hint_style) filtered_search.set('author:') + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) find('.filtered-search-input-container .clear-search').click filtered_search.click + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset) end From 3e457f78cf6798042a1fc30790c24f935861182f Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 11 Jan 2017 21:12:03 -0500 Subject: [PATCH 185/185] Fix rubocop --- .../filtered_search/filter_issues_spec.rb | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 7cae7152aa2..ead43d6784a 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -107,12 +107,12 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by invalid author' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by multiple authors' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end @@ -152,7 +152,7 @@ describe 'Filter issues', js: true, feature: true do it 'sorting' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end @@ -176,12 +176,12 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by invalid assignee' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by multiple assignees' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end @@ -222,7 +222,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end end @@ -247,7 +247,7 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by invalid label' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by multiple labels' do @@ -423,7 +423,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end end @@ -450,12 +450,12 @@ describe 'Filter issues', js: true, feature: true do it 'filters issues by invalid milestones' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by multiple milestones' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end it 'filters issues by milestone containing special characters' do @@ -517,7 +517,7 @@ describe 'Filter issues', js: true, feature: true do context 'sorting' do it 'sorts' do pending('to be tested, issue #26546') - fail + expect(true).to be(false) end end end