From 71dc5af9ce5e25d8d3219b296e23c3ca6340451b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Fri, 4 Nov 2016 16:27:11 -0500 Subject: [PATCH] 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"