summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/dispatcher.js.es63
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js13
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es6104
-rw-r--r--app/assets/stylesheets/framework/filters.scss24
-rw-r--r--app/views/projects/issues/index.html.haml6
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml76
-rw-r--r--config/application.rb1
7 files changed, 225 insertions, 2 deletions
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"