From a97c51580ee996046725d570bc36b96feb9982b5 Mon Sep 17 00:00:00 2001 From: Chia Yu Pai Date: Fri, 15 Jan 2016 14:22:36 +0800 Subject: [PATCH 001/264] Update github.md add default callback URL path --- doc/integration/github.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/integration/github.md b/doc/integration/github.md index a789d2c814f..dbcb2d6a006 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -18,6 +18,7 @@ GitHub will generate an application ID and secret key for you to use. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - Authorization callback URL: 'https://gitlab.company.com/' + - If install from source, default callback URL is '${YOUR_DOMAIN}/import/github/callback' 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). From da866795edcf77298741f7acc70dd49daeec9eed Mon Sep 17 00:00:00 2001 From: Chia Yu Pai Date: Tue, 1 Mar 2016 18:30:22 +0800 Subject: [PATCH 002/264] Update github default callback url --- doc/integration/github.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/integration/github.md b/doc/integration/github.md index dbcb2d6a006..ce2434330ea 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -17,8 +17,7 @@ GitHub will generate an application ID and secret key for you to use. - Application name: This can be anything. Consider something like "\'s GitLab" or "\'s GitLab" or something else descriptive. - Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com' - Application description: Fill this in if you wish. - - Authorization callback URL: 'https://gitlab.company.com/' - - If install from source, default callback URL is '${YOUR_DOMAIN}/import/github/callback' + - Default authorization callback URL is '${YOUR_DOMAIN}/import/github/callback' 1. Select "Register application". 1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). From e8c723543cfc4c1d905a5794a2da1bef7689d784 Mon Sep 17 00:00:00 2001 From: Baldinof Date: Wed, 9 Mar 2016 15:25:48 +0100 Subject: [PATCH 003/264] Close merge requests when removing fork relation --- CHANGELOG | 1 + app/controllers/projects_controller.rb | 2 +- app/models/merge_request.rb | 1 + app/models/project.rb | 12 +++++++++++- spec/models/project_spec.rb | 19 +++++++++++++++++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c1c90903d5d..84739fab82b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -26,6 +26,7 @@ v 8.6.0 (unreleased) - Show labels in dashboard and group milestone views - Add main language of a project in the list of projects (Tiago Botelho) - Add ability to show archived projects on dashboard, explore and group pages + - Remove fork link closes all merge requests opened on source project (Florent Baldino) v 8.5.5 - Ensure removing a project removes associated Todo entries diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index aea08ecce3e..a26d11459f0 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -72,7 +72,7 @@ class ProjectsController < ApplicationController def remove_fork return access_denied! unless can?(current_user, :remove_fork_project, @project) - if @project.unlink_fork + if @project.unlink_fork(current_user) flash[:notice] = 'The fork relationship has been removed.' end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c1e18bb3cc5..18ec48b57f4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -137,6 +137,7 @@ class MergeRequest < ActiveRecord::Base scope :by_milestone, ->(milestone) { where(milestone_id: milestone) } scope :in_projects, ->(project_ids) { where("source_project_id in (:project_ids) OR target_project_id in (:project_ids)", project_ids: project_ids) } scope :of_projects, ->(ids) { where(target_project_id: ids) } + scope :from_project, ->(project) { where(source_project_id: project.id) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } diff --git a/app/models/project.rb b/app/models/project.rb index 65829bec77a..859758293e1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -890,12 +890,22 @@ class Project < ActiveRecord::Base self.builds_enabled = true end - def unlink_fork + def unlink_fork(user) if forked? forked_from_project.lfs_objects.find_each do |lfs_object| lfs_object.projects << self end + merge_requests = forked_from_project.merge_requests.opened.from_project(self) + + unless merge_requests.empty? + close_service = MergeRequests::CloseService.new(self, user) + + merge_requests.each do |mr| + close_service.execute(mr) + end + end + forked_project_link.destroy end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2fa38a5d3d3..ba4fb2f8222 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -647,4 +647,23 @@ describe Project, models: true do project.expire_caches_before_rename('foo') end end + + describe '#unlink_fork' do + let(:fork_link) { create(:forked_project_link) } + let(:fork_project) { fork_link.forked_to_project } + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) } + let!(:close_service) { MergeRequests::CloseService.new(fork_project, user) } + + it 'remove fork relation and close all pending merge requests' do + allow(MergeRequests::CloseService).to receive(:new). + with(fork_project, user). + and_return(close_service) + + expect(close_service).to receive(:execute).with(merge_request) + expect(fork_project.forked_project_link).to receive(:destroy) + + fork_project.unlink_fork(user) + end + end end From 4b3d344688954e9c515b9bb8f26239a781fcabfb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 8 Mar 2016 02:56:43 -0500 Subject: [PATCH 004/264] Working version of autocomplete with categorized results --- app/assets/javascripts/dispatcher.js.coffee | 7 +- .../lib/category_autocomplete.js.coffee | 17 ++ .../javascripts/search_autocomplete.js.coffee | 169 +++++++++++++++++- app/helpers/search_helper.rb | 59 +++--- app/views/layouts/_search.html.haml | 13 +- app/views/shared/_location_badge.html.haml | 13 ++ 6 files changed, 229 insertions(+), 49 deletions(-) create mode 100644 app/assets/javascripts/lib/category_autocomplete.js.coffee create mode 100644 app/views/shared/_location_badge.html.haml diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 1be86e3b820..0aefea7d8d9 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -151,9 +151,4 @@ class Dispatcher new Shortcuts() initSearch: -> - opts = $('.search-autocomplete-opts') - path = opts.data('autocomplete-path') - project_id = opts.data('autocomplete-project-id') - project_ref = opts.data('autocomplete-project-ref') - - new SearchAutocomplete(path, project_id, project_ref) + new SearchAutocomplete() diff --git a/app/assets/javascripts/lib/category_autocomplete.js.coffee b/app/assets/javascripts/lib/category_autocomplete.js.coffee new file mode 100644 index 00000000000..490032dc782 --- /dev/null +++ b/app/assets/javascripts/lib/category_autocomplete.js.coffee @@ -0,0 +1,17 @@ +$.widget( "custom.catcomplete", $.ui.autocomplete, + _create: -> + @_super(); + @widget().menu("option", "items", "> :not(.ui-autocomplete-category)") + + _renderMenu: (ul, items) -> + currentCategory = '' + $.each items, (index, item) => + if item.category isnt currentCategory + ul.append("
  • #{item.category}
  • ") + currentCategory = item.category + + li = @_renderItemData(ul, item) + + if item.category? + li.attr('aria-label', item.category + " : " + item.label) + ) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index c1801365266..df31b07910c 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,11 +1,164 @@ class @SearchAutocomplete - constructor: (search_autocomplete_path, project_id, project_ref) -> - project_id = '' unless project_id - project_ref = '' unless project_ref - query = "?project_id=" + project_id + "&project_ref=" + project_ref + constructor: (opts = {}) -> + { + @wrap = $('.search') + @optsEl = @wrap.find('.search-autocomplete-opts') + @autocompletePath = @optsEl.data('autocomplete-path') + @projectId = @optsEl.data('autocomplete-project-id') || '' + @projectRef = @optsEl.data('autocomplete-project-ref') || '' + } = opts - $("#search").autocomplete - source: search_autocomplete_path + query + @keyCode = + ESCAPE: 27 + BACKSPACE: 8 + TAB: 9 + ENTER: 13 + + @locationBadgeEl = @$('.search-location-badge') + @locationText = @$('.location-text') + @searchInput = @$('.search-input') + @projectInputEl = @$('#project_id') + @groupInputEl = @$('#group_id') + @searchCodeInputEl = @$('#search_code') + @repositoryInputEl = @$('#repository_ref') + @scopeInputEl = @$('#scope') + + @saveOriginalState() + @createAutocomplete() + @bindEvents() + + $: (selector) -> + @wrap.find(selector) + + saveOriginalState: -> + @originalState = @serializeState() + + restoreOriginalState: -> + inputs = Object.keys @originalState + + for input in inputs + @$("##{input}").val(@originalState[input]) + + + if @originalState._location is '' + @locationBadgeEl.html('') + else + @addLocationBadge( + value: @originalState._location + ) + + serializeState: -> + { + # Search Criteria + project_id: @projectInputEl.val() + group_id: @groupInputEl.val() + search_code: @searchCodeInputEl.val() + repository_ref: @repositoryInputEl.val() + + # Location badge + _location: $.trim(@locationText.text()) + } + + createAutocomplete: -> + @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef + + @catComplete = @searchInput.catcomplete + appendTo: 'form.navbar-form' + source: @autocompletePath + @query minLength: 1 - select: (event, ui) -> - location.href = ui.item.url + close: (e) -> + e.preventDefault() + + select: (event, ui) => + # Pressing enter choses an alternative + if event.keyCode is @keyCode.ENTER + @goToResult(ui.item) + else + # Pressing tab sets the scope + if event.keyCode is @keyCode.TAB and ui.item.scope? + @setLocationBadge(ui.item) + @searchInput + .val('') # remove selected value from input + .focus() + else + # If option is not a scope go to page + @goToResult(ui.item) + + # Return false to avoid focus on the next element + return false + + + bindEvents: -> + @searchInput.on 'keydown', @onSearchKeyDown + @wrap.on 'click', '.remove-badge', @onRemoveLocationBadgeClick + + onRemoveLocationBadgeClick: (e) => + e.preventDefault() + @removeLocationBadge() + @searchInput.focus() + + onSearchKeyDown: (e) => + # Remove tag when pressing backspace and input search is empty + if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is '' + @removeLocationBadge() + @destroyAutocomplete() + @searchInput.focus() + else if e.keyCode is @keyCode.ESCAPE + @restoreOriginalState() + else + # Create new autocomplete instance if it's not created + @createAutocomplete() unless @catcomplete? + + addLocationBadge: (item) -> + category = if item.category? then "#{item.category}: " else '' + value = if item.value? then item.value else '' + + html = " + #{category}#{value} + x + " + @locationBadgeEl.html(html) + + setLocationBadge: (item) -> + @addLocationBadge(item) + + # Reset input states + @resetSearchState() + + switch item.scope + when 'projects' + @projectInputEl.val(item.id) + # @searchCodeInputEl.val('true') # TODO: always true for projects? + # @repositoryInputEl.val('master') # TODO: always master? + + when 'groups' + @groupInputEl.val(item.id) + + removeLocationBadge: -> + @locationBadgeEl.empty() + + # Reset state + @resetSearchState() + + resetSearchState: -> + # Remove scope + @scopeInputEl.val('') + + # Remove group + @groupInputEl.val('') + + # Remove project id + @projectInputEl.val('') + + # Remove code search + @searchCodeInputEl.val('') + + # Remove repository ref + @repositoryInputEl.val('') + + goToResult: (result) -> + location.href = result.url + + destroyAutocomplete: -> + @catComplete.destroy() if @catcomplete? + @catComplete = null diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 494dad0b41e..9102fd6d501 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -23,45 +23,45 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { label: "Profile settings", url: profile_path }, - { label: "SSH Keys", url: profile_keys_path }, - { label: "Dashboard", url: root_path }, - { label: "Admin Section", url: admin_root_path }, + { category: "Settings", label: "Profile settings", url: profile_path }, + { category: "Settings", label: "SSH Keys", url: profile_keys_path }, + { category: "Settings", label: "Dashboard", url: root_path }, + { category: "Settings", label: "Admin Section", url: admin_root_path }, ] end # Autocomplete results for internal help pages def help_autocomplete [ - { label: "help: API Help", url: help_page_path("api", "README") }, - { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, - { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, - { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, - { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, - { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, - { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, - { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, + { category: "Help", label: "API Help", url: help_page_path("api", "README") }, + { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") }, + { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") }, + { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") }, + { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") }, + { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") }, + { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, + { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") }, ] end # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - prefix = search_result_sanitize(@project.name_with_namespace) + prefix = "Project - " + search_result_sanitize(@project.name_with_namespace) ref = @ref || @project.repository.root_ref [ - { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: prefix, label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { category: prefix, label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { category: prefix, label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { category: prefix, label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { category: prefix, label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { category: prefix, label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { category: prefix, label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { category: prefix, label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { category: prefix, label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: prefix, label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] @@ -72,7 +72,10 @@ module SearchHelper def groups_autocomplete(term, limit = 5) current_user.authorized_groups.search(term).limit(limit).map do |group| { - label: "group: #{search_result_sanitize(group.name)}", + category: "Groups", + scope: "groups", + id: group.id, + label: "#{search_result_sanitize(group.name)}", url: group_path(group) } end @@ -83,7 +86,11 @@ module SearchHelper current_user.authorized_projects.search_by_title(term). sorted_by_stars.non_archived.limit(limit).map do |p| { - label: "project: #{search_result_sanitize(p.name_with_namespace)}", + category: "Projects", + scope: "projects", + id: p.id, + value: "#{search_result_sanitize(p.name)}", + label: "#{search_result_sanitize(p.name_with_namespace)}", url: namespace_project_path(p.namespace, p) } end diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 54af2c3063c..c5002893831 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,10 +1,12 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| + = render 'shared/location_badge' = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1" = hidden_field_tag :group_id, @group.try(:id) - - if @project && @project.persisted? - = hidden_field_tag :project_id, @project.id + = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '' + + - if @project && @project.persisted? - if current_controller?(:issues) = hidden_field_tag :scope, 'issues' - elsif current_controller?(:merge_requests) @@ -21,10 +23,3 @@ = hidden_field_tag :repository_ref, @ref = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } - -:javascript - $('.search-input').on('keyup', function(e) { - if (e.keyCode == 27) { - $('.search-input').blur(); - } - }); diff --git a/app/views/shared/_location_badge.html.haml b/app/views/shared/_location_badge.html.haml new file mode 100644 index 00000000000..dfe8bc010d6 --- /dev/null +++ b/app/views/shared/_location_badge.html.haml @@ -0,0 +1,13 @@ +- if controller.controller_path =~ /^groups/ + - label = 'This group' +- if controller.controller_path =~ /^projects/ + - label = 'This project' + +.search-location-badge + - if label.present? + %span.label.label-primary + %i.location-text + = label + + %a.remove-badge{href: '#'} + x From 6f449c63ddab0027ef064b436d98c8e820cbe7b3 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 8 Mar 2016 19:39:14 -0500 Subject: [PATCH 005/264] Apply styling and tweaks to autocomplete dropdown --- .../lib/category_autocomplete.js.coffee | 32 +++++++++ .../javascripts/search_autocomplete.js.coffee | 35 ++++++++-- app/assets/stylesheets/framework/forms.scss | 34 --------- app/assets/stylesheets/framework/header.scss | 20 ------ app/assets/stylesheets/framework/jquery.scss | 36 ++++++++-- app/assets/stylesheets/pages/search.scss | 70 +++++++++++++++++++ app/helpers/search_helper.rb | 21 +++--- app/views/layouts/_search.html.haml | 13 ++-- app/views/shared/_location_badge.html.haml | 13 ++-- 9 files changed, 186 insertions(+), 88 deletions(-) diff --git a/app/assets/javascripts/lib/category_autocomplete.js.coffee b/app/assets/javascripts/lib/category_autocomplete.js.coffee index 490032dc782..c85fabbcd5b 100644 --- a/app/assets/javascripts/lib/category_autocomplete.js.coffee +++ b/app/assets/javascripts/lib/category_autocomplete.js.coffee @@ -14,4 +14,36 @@ $.widget( "custom.catcomplete", $.ui.autocomplete, if item.category? li.attr('aria-label', item.category + " : " + item.label) + + _renderItem: (ul, item) -> + # Highlight occurrences + item.label = item.label.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + $.ui.autocomplete.escapeRegex(this.term) + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); + + return $( "
  • " ) + .data( "item.autocomplete", item ) + .append( "#{item.label}" ) + .appendTo( ul ); + + _resizeMenu: -> + if (isNaN(this.options.maxShowItems)) + return + + ul = this.menu.element.css(overflowX: '', overflowY: '', width: '', maxHeight: '') + + lis = ul.children('li').css('whiteSpace', 'nowrap'); + + if (lis.length > this.options.maxShowItems) + ulW = ul.prop('clientWidth') + + ul.css( + overflowX: 'hidden' + overflowY: 'auto' + maxHeight: lis.eq(0).outerHeight() * this.options.maxShowItems + 1 + ) + + barW = ulW - ul.prop('clientWidth'); + ul.width('+=' + barW); + + # Original code from jquery.ui.autocomplete.js _resizeMenu() + ul.outerWidth(Math.max(ul.outerWidth() + 1, this.element.outerWidth())); ) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index df31b07910c..a6d5ab65239 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -24,7 +24,10 @@ class @SearchAutocomplete @scopeInputEl = @$('#scope') @saveOriginalState() - @createAutocomplete() + + if @locationBadgeEl.is(':empty') + @createAutocomplete() + @bindEvents() $: (selector) -> @@ -66,6 +69,12 @@ class @SearchAutocomplete appendTo: 'form.navbar-form' source: @autocompletePath + @query minLength: 1 + maxShowItems: 15 + position: + # { my: "left top", at: "left bottom", collision: "none" } + my: "left-10 top+9" + at: "left bottom" + collision: "none" close: (e) -> e.preventDefault() @@ -89,7 +98,9 @@ class @SearchAutocomplete bindEvents: -> - @searchInput.on 'keydown', @onSearchKeyDown + @searchInput.on 'keydown', @onSearchInputKeyDown + @searchInput.on 'focus', @onSearchInputFocus + @searchInput.on 'blur', @onSearchInputBlur @wrap.on 'click', '.remove-badge', @onRemoveLocationBadgeClick onRemoveLocationBadgeClick: (e) => @@ -97,7 +108,7 @@ class @SearchAutocomplete @removeLocationBadge() @searchInput.focus() - onSearchKeyDown: (e) => + onSearchInputKeyDown: (e) => # Remove tag when pressing backspace and input search is empty if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is '' @removeLocationBadge() @@ -106,14 +117,24 @@ class @SearchAutocomplete else if e.keyCode is @keyCode.ESCAPE @restoreOriginalState() else - # Create new autocomplete instance if it's not created - @createAutocomplete() unless @catcomplete? + # Create new autocomplete if hasn't been created yet and there's no badge + if !@catComplete? and @locationBadgeEl.is(':empty') + @createAutocomplete() + + onSearchInputFocus: => + @wrap.addClass('search-active') + + onSearchInputBlur: => + @wrap.removeClass('search-active') + + # If input is blank then restore state + @restoreOriginalState() if @searchInput.val() is '' addLocationBadge: (item) -> category = if item.category? then "#{item.category}: " else '' value = if item.value? then item.value else '' - html = " + html = " #{category}#{value} x " @@ -160,5 +181,5 @@ class @SearchAutocomplete location.href = result.url destroyAutocomplete: -> - @catComplete.destroy() if @catcomplete? + @catComplete.destroy() if @catComplete? @catComplete = null diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 6c08005812e..18136509da5 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -6,40 +6,6 @@ input { border-radius: $border-radius-base; } -input[type='search'] { - background-color: white; - padding-left: 10px; -} - -input[type='search'].search-input { - background-repeat: no-repeat; - background-position: 10px; - background-size: 16px; - background-position-x: 30%; - padding-left: 10px; - background-color: $gray-light; - - &.search-input[value=""] { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC'); - } - - &.search-input::-webkit-input-placeholder { - text-align: center; - } - - &.search-input:-moz-placeholder { /* Firefox 18- */ - text-align: center; - } - - &.search-input::-moz-placeholder { /* Firefox 19+ */ - text-align: center; - } - - &.search-input:-ms-input-placeholder { - text-align: center; - } -} - input[type='text'].danger { background: #F2DEDE!important; border-color: #D66; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 4c4033e3ae7..f72bd223486 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -112,26 +112,6 @@ header { } } - .search { - margin-right: 10px; - margin-left: 10px; - margin-top: ($header-height - 36) / 2; - - form { - margin: 0; - padding: 0; - } - - .search-input { - width: 220px; - - &:focus { - @include box-shadow(none); - outline: none; - } - } - } - .impersonation i { color: $red-normal; } diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 0cdcd923b3c..76b4cea4778 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -19,13 +19,41 @@ } &.ui-autocomplete { - border-color: #DDD; - padding: 0; margin-top: 2px; z-index: 1001; + width: 240px; + margin-bottom: 0; + padding: 10px 10px; + font-size: 14px; + font-weight: normal; + background-color: $dropdown-bg; + border: 1px solid $dropdown-border-color; + border-radius: $border-radius-base; + box-shadow: 0 2px 4px $dropdown-shadow-color; - .ui-menu-item a { - padding: 4px 10px; + .ui-menu-item { + display: block; + position: relative; + padding: 0 10px; + color: $dropdown-link-color; + line-height: 34px; + text-overflow: ellipsis; + border-radius: 2px; + white-space: nowrap; + overflow: hidden; + border: none; + + &.ui-state-focus { + background-color: $dropdown-link-hover-bg; + text-decoration: none; + margin: 0; + } + } + + .ui-autocomplete-category { + text-transform: uppercase; + font-size: 11px; + color: #7f8fa4; } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 84234b15c65..3c3313c911b 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -21,3 +21,73 @@ } } + +.search { + margin-right: 10px; + margin-left: 10px; + margin-top: ($header-height - 35) / 2; + + &.search-active { + form { + @extend .form-control:focus; + } + + .location-badge { + @include transition(all .15s); + background-color: $input-border-focus; + color: $white-light; + } + } + + form { + @extend .form-control; + margin: 0; + padding: 4px; + width: 350px; + line-height: 24px; + overflow: hidden; + } + + .location-text { + font-style: normal; + } + + .remove-badge { + display: none; + } + + .search-input { + border: none; + font-size: 14px; + outline: none; + padding: 0; + margin-left: 2px; + line-height: 25px; + width: 100%; + } + + .location-badge { + line-height: 25px; + padding: 0 5px; + border-radius: 2px; + font-size: 14px; + font-style: normal; + color: #AAAAAA; + display: inline-block; + background-color: #F5F5F5; + vertical-align: top; + } + + .search-input-container { + display: flex; + } + + .search-location-badge, .search-input-wrap { + // Fallback if flex is not supported + display: inline-block; + } + + .search-input-wrap { + width: 100%; + } +} diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 9102fd6d501..cbead1b8b74 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -48,20 +48,19 @@ module SearchHelper # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - prefix = "Project - " + search_result_sanitize(@project.name_with_namespace) ref = @ref || @project.repository.root_ref [ - { category: prefix, label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { category: prefix, label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { category: prefix, label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { category: prefix, label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { category: prefix, label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { category: prefix, label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { category: prefix, label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { category: prefix, label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { category: prefix, label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { category: prefix, label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index c5002893831..843c833b4fe 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,9 +1,12 @@ -.search - = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = render 'shared/location_badge' - = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1" - = hidden_field_tag :group_id, @group.try(:id) +.search.search-form + = form_tag search_path, method: :get, class: 'navbar-form' do |f| + .search-input-container + .search-location-badge + = render 'shared/location_badge' + .search-input-wrap + = search_field_tag "search", nil, placeholder: 'Search', class: "search-input", spellcheck: false, tabindex: "1", autocomplete: 'off' + = hidden_field_tag :group_id, @group.try(:id) = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '' - if @project && @project.persisted? diff --git a/app/views/shared/_location_badge.html.haml b/app/views/shared/_location_badge.html.haml index dfe8bc010d6..f1ecc060cf1 100644 --- a/app/views/shared/_location_badge.html.haml +++ b/app/views/shared/_location_badge.html.haml @@ -3,11 +3,10 @@ - if controller.controller_path =~ /^projects/ - label = 'This project' -.search-location-badge - - if label.present? - %span.label.label-primary - %i.location-text - = label +- if label.present? + %span.location-badge + %i.location-text + = label - %a.remove-badge{href: '#'} - x + %a.remove-badge{href: '#'} + x From d6f822423d0f9c0d463cc25469833009815eae4a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 8 Mar 2016 21:26:24 -0500 Subject: [PATCH 006/264] Tweak behaviours --- .../javascripts/search_autocomplete.js.coffee | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index a6d5ab65239..3cedf1c7b12 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -25,7 +25,8 @@ class @SearchAutocomplete @saveOriginalState() - if @locationBadgeEl.is(':empty') + # If there's no location badge + if !@locationBadgeEl.children().length @createAutocomplete() @bindEvents() @@ -65,7 +66,7 @@ class @SearchAutocomplete createAutocomplete: -> @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef - @catComplete = @searchInput.catcomplete + @searchInput.catcomplete appendTo: 'form.navbar-form' source: @autocompletePath + @query minLength: 1 @@ -96,6 +97,7 @@ class @SearchAutocomplete # Return false to avoid focus on the next element return false + @autocomplete = @searchInput.data 'customCatcomplete' bindEvents: -> @searchInput.on 'keydown', @onSearchInputKeyDown @@ -112,14 +114,19 @@ class @SearchAutocomplete # Remove tag when pressing backspace and input search is empty if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is '' @removeLocationBadge() - @destroyAutocomplete() + # @destroyAutocomplete() @searchInput.focus() else if e.keyCode is @keyCode.ESCAPE @restoreOriginalState() else # Create new autocomplete if hasn't been created yet and there's no badge - if !@catComplete? and @locationBadgeEl.is(':empty') - @createAutocomplete() + if @autocomplete is undefined + if !@locationBadgeEl.children().length + @createAutocomplete() + else + # There's a badge + if @locationBadgeEl.children().length + @destroyAutocomplete() onSearchInputFocus: => @wrap.addClass('search-active') @@ -181,5 +188,6 @@ class @SearchAutocomplete location.href = result.url destroyAutocomplete: -> - @catComplete.destroy() if @catComplete? - @catComplete = null + @autocomplete.destroy() if @autocomplete isnt undefined + @searchInput.attr('autocomplete', 'off') + @autocomplete = undefined From f825b60b918a115a5a4a8d66abbbf35a10653b1a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 12:45:43 -0500 Subject: [PATCH 007/264] Change hidden input id to avoid duplicated IDs The TODOs dashboard already had a #project_id input and it was causing a spec to fail --- app/assets/javascripts/search_autocomplete.js.coffee | 2 +- app/views/layouts/_search.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 3cedf1c7b12..0c4876358bd 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -17,7 +17,7 @@ class @SearchAutocomplete @locationBadgeEl = @$('.search-location-badge') @locationText = @$('.location-text') @searchInput = @$('.search-input') - @projectInputEl = @$('#project_id') + @projectInputEl = @$('#search_project_id') @groupInputEl = @$('#group_id') @searchCodeInputEl = @$('#search_code') @repositoryInputEl = @$('#repository_ref') diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 843c833b4fe..58a3cdf955e 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -7,7 +7,7 @@ = search_field_tag "search", nil, placeholder: 'Search', class: "search-input", spellcheck: false, tabindex: "1", autocomplete: 'off' = hidden_field_tag :group_id, @group.try(:id) - = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '' + = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' - if @project && @project.persisted? - if current_controller?(:issues) From 1879057ced32a33c5204f5903f0e7c931d942b58 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 15:52:15 -0500 Subject: [PATCH 008/264] Add icons --- app/assets/images/spinner.svg | 1 + app/assets/stylesheets/pages/search.scss | 39 +++++++++++++++++++++++- app/views/layouts/_search.html.haml | 1 + 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/spinner.svg diff --git a/app/assets/images/spinner.svg b/app/assets/images/spinner.svg new file mode 100644 index 00000000000..3dd110cfa0f --- /dev/null +++ b/app/assets/images/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 3c3313c911b..90c9d4de59d 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -37,6 +37,12 @@ background-color: $input-border-focus; color: $white-light; } + + .search-input-wrap { + i { + color: $input-border-focus; + } + } } form { @@ -61,7 +67,7 @@ font-size: 14px; outline: none; padding: 0; - margin-left: 2px; + margin-left: 5px; line-height: 25px; width: 100%; } @@ -89,5 +95,36 @@ .search-input-wrap { width: 100%; + position: relative; + + .search-icon { + @extend .fa-search; + @include transition(color .15s); + position: absolute; + right: 5px; + color: #E7E9ED; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + + &:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + } + } + + .ui-autocomplete-loading + .search-icon { + height: 25px; + width: 25px; + position: absolute; + right: 0; + background-image: image-url('spinner.svg'); + fill: red; + + &:before { + display: none; + } + } } } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 58a3cdf955e..a004908fb6f 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -5,6 +5,7 @@ = render 'shared/location_badge' .search-input-wrap = search_field_tag "search", nil, placeholder: 'Search', class: "search-input", spellcheck: false, tabindex: "1", autocomplete: 'off' + %i.search-icon = hidden_field_tag :group_id, @group.try(:id) = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' From 651e893d63f50a457d20705401b80414a86d0918 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 16:34:21 -0500 Subject: [PATCH 009/264] Better wording --- app/assets/javascripts/search_autocomplete.js.coffee | 8 ++++---- app/helpers/search_helper.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 0c4876358bd..b8671900862 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -84,14 +84,14 @@ class @SearchAutocomplete if event.keyCode is @keyCode.ENTER @goToResult(ui.item) else - # Pressing tab sets the scope - if event.keyCode is @keyCode.TAB and ui.item.scope? + # Pressing tab sets the location + if event.keyCode is @keyCode.TAB and ui.item.location? @setLocationBadge(ui.item) @searchInput .val('') # remove selected value from input .focus() else - # If option is not a scope go to page + # If option is not a location go to page @goToResult(ui.item) # Return false to avoid focus on the next element @@ -153,7 +153,7 @@ class @SearchAutocomplete # Reset input states @resetSearchState() - switch item.scope + switch item.location when 'projects' @projectInputEl.val(item.id) # @searchCodeInputEl.val('true') # TODO: always true for projects? diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index cbead1b8b74..de164547396 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -72,7 +72,7 @@ module SearchHelper current_user.authorized_groups.search(term).limit(limit).map do |group| { category: "Groups", - scope: "groups", + location: "groups", id: group.id, label: "#{search_result_sanitize(group.name)}", url: group_path(group) @@ -86,7 +86,7 @@ module SearchHelper sorted_by_stars.non_archived.limit(limit).map do |p| { category: "Projects", - scope: "projects", + location: "projects", id: p.id, value: "#{search_result_sanitize(p.name)}", label: "#{search_result_sanitize(p.name_with_namespace)}", From 8048d6114861988ea7d0325a58f26812dd48fd09 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 22:16:30 -0500 Subject: [PATCH 010/264] Replace spinner icon for th FontAwesome one --- app/assets/images/spinner.svg | 1 - app/assets/stylesheets/pages/search.scss | 12 ++---------- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 app/assets/images/spinner.svg diff --git a/app/assets/images/spinner.svg b/app/assets/images/spinner.svg deleted file mode 100644 index 3dd110cfa0f..00000000000 --- a/app/assets/images/spinner.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 90c9d4de59d..bc660985ecb 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -115,16 +115,8 @@ } .ui-autocomplete-loading + .search-icon { - height: 25px; - width: 25px; - position: absolute; - right: 0; - background-image: image-url('spinner.svg'); - fill: red; - - &:before { - display: none; - } + @extend .fa-spinner; + @extend .fa-spin; } } } From 2f4bdefc725728473fa339a79c8813e6015a4667 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 13:18:38 -0500 Subject: [PATCH 011/264] Allow to pass non-asynchronous data to GitLabDropdown --- app/assets/javascripts/gl_dropdown.js.coffee | 27 +++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 4f038477755..e763ca5c780 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -83,15 +83,19 @@ class GitLabDropdown search_fields = if @options.search then @options.search.fields else []; if @options.data - # Remote data - @remote = new GitLabDropdownRemote @options.data, { - dataType: @options.dataType, - beforeSend: @toggleLoading.bind(@) - success: (data) => - @fullData = data + # If data is an array + if _.isArray @options.data + @parseData @options.data + else + # Remote data + @remote = new GitLabDropdownRemote @options.data, { + dataType: @options.dataType, + beforeSend: @toggleLoading.bind(@) + success: (data) => + @fullData = data - @parseData @fullData - } + @parseData @fullData + } # Init filiterable if @options.filterable @@ -204,7 +208,12 @@ class GitLabDropdown else selected = if @options.isSelected then @options.isSelected(data) else false url = if @options.url then @options.url(data) else "#" - text = if @options.text then @options.text(data) else "" + + if @options.text? + text = @options.text(data) + else + text = data.text if data.text? + cssClass = ""; if selected From 761a8d98e82fdce5b04d3e50e20a13e9d35a9919 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 13:39:28 -0500 Subject: [PATCH 012/264] Allow data with desired format --- app/assets/javascripts/gl_dropdown.js.coffee | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index e763ca5c780..0b0620a71c5 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -209,10 +209,17 @@ class GitLabDropdown selected = if @options.isSelected then @options.isSelected(data) else false url = if @options.url then @options.url(data) else "#" + # Set URL + if @options.url? + url = @options.url(data) + else + url = if data.url? then data.url else '' + + # Set Text if @options.text? text = @options.text(data) else - text = data.text if data.text? + text = if data.text? then data.text else '' cssClass = ""; From 238328f56e16fe53ef0014c249e932fdb6260568 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 14:59:50 -0500 Subject: [PATCH 013/264] Allow to pass input filter param This allow us to set a different input to filter results --- app/assets/javascripts/gl_dropdown.js.coffee | 28 +++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 0b0620a71c5..1d100c054d7 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -2,7 +2,9 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] constructor: (@dropdown, @options) -> - @input = @dropdown.find(".dropdown-input .dropdown-input-field") + { + @input + } = @options # Key events timeout = "" @@ -77,14 +79,30 @@ class GitLabDropdown PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + FILTER_INPUT = '.dropdown-input .dropdown-input-field' + constructor: (@el, @options) -> - self = @ @dropdown = $(@el).parent() + + # Set Defaults + { + # If no input is passed create a default one + @filterInput = @$(FILTER_INPUT) + } = @options + + self = @ + + # If selector was passed + if _.isString(@filterInput) + @filterInput = @$(@filterInput) + + search_fields = if @options.search then @options.search.fields else []; if @options.data # If data is an array if _.isArray @options.data + @fullData = @options.data @parseData @options.data else # Remote data @@ -100,6 +118,7 @@ class GitLabDropdown # Init filiterable if @options.filterable @filter = new GitLabDropdownFilter @dropdown, + input: @filterInput remote: @options.filterRemote query: @options.data keys: @options.search.fields @@ -133,6 +152,9 @@ class GitLabDropdown if self.options.clicked self.options.clicked() + $: (selector) -> + $(selector, @dropdown) + toggleLoading: -> $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS @@ -167,7 +189,7 @@ class GitLabDropdown @remote.execute() if @options.filterable - @dropdown.find(".dropdown-input-field").focus() + @filterInput.focus() hidden: => if @options.filterable From 03afe76614aebb0bc81c8ed42869b0887b6abe6c Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 18:00:17 -0500 Subject: [PATCH 014/264] Allow to pass header items --- app/assets/javascripts/gl_dropdown.js.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 1d100c054d7..042ae1d04b6 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -222,8 +222,12 @@ class GitLabDropdown renderItem: (data) -> html = "" + # Separator return "
  • " if data is "divider" + # Header + return "" if data.header? + if @options.renderRow # Call the render function html = @options.renderRow(data) From 424927a7173f7038448827b862b400e2df50048a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 20:38:19 -0500 Subject: [PATCH 015/264] Allow to hightlight matches --- app/assets/javascripts/gl_dropdown.js.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 042ae1d04b6..62199e5be07 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -252,6 +252,8 @@ class GitLabDropdown if selected cssClass = "is-active" + text = @highlightTextMatches(text, @filterInput.val()) + html = "
  • " html += "" html += text @@ -260,6 +262,15 @@ class GitLabDropdown return html + highlightTextMatches: (text, term) -> + occurrences = fuzzaldrinPlus.match(text, term) + textArr = text.split('') + textArr.forEach (character, i, textArr) -> + if i in occurrences + textArr[i] = "#{character}" + + textArr.join '' + noResults: -> html = "
  • " html += "" From dce5e9ce4824b62ef939aa635357a813a858322e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 20:47:01 -0500 Subject: [PATCH 016/264] Disable highlighting by default --- app/assets/javascripts/gl_dropdown.js.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 62199e5be07..79696cc679d 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -88,6 +88,7 @@ class GitLabDropdown { # If no input is passed create a default one @filterInput = @$(FILTER_INPUT) + @highlight = false } = @options self = @ @@ -252,7 +253,8 @@ class GitLabDropdown if selected cssClass = "is-active" - text = @highlightTextMatches(text, @filterInput.val()) + if @highlight + text = @highlightTextMatches(text, @filterInput.val()) html = "
  • " html += "" From d38ef7b5b07890d02256bf05cf6b126fceee5770 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 14 Mar 2016 16:14:29 -0500 Subject: [PATCH 017/264] Use new dropdown class for search suggestions --- app/assets/javascripts/gl_dropdown.js.coffee | 5 +- .../javascripts/search_autocomplete.js.coffee | 266 +++++++++--------- app/assets/stylesheets/framework/jquery.scss | 6 - app/assets/stylesheets/pages/search.scss | 13 +- app/views/layouts/_search.html.haml | 6 +- 5 files changed, 154 insertions(+), 142 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 79696cc679d..0684e7852fa 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -4,6 +4,7 @@ class GitLabDropdownFilter constructor: (@dropdown, @options) -> { @input + @filterInputBlur = true } = @options # Key events @@ -19,7 +20,7 @@ class GitLabDropdownFilter blur_field = @shouldBlur e.keyCode search_text = @input.val() - if blur_field + if blur_field && @filterInputBlur @input.blur() if @options.remote @@ -89,6 +90,7 @@ class GitLabDropdown # If no input is passed create a default one @filterInput = @$(FILTER_INPUT) @highlight = false + @filterInputBlur = true } = @options self = @ @@ -119,6 +121,7 @@ class GitLabDropdown # Init filiterable if @options.filterable @filter = new GitLabDropdownFilter @dropdown, + filterInputBlur: @filterInputBlur input: @filterInput remote: @options.filterRemote query: @options.data diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index b8671900862..e21a140b2a6 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,21 +1,28 @@ class @SearchAutocomplete + + KEYCODE = + ESCAPE: 27 + BACKSPACE: 8 + TAB: 9 + ENTER: 13 + constructor: (opts = {}) -> { @wrap = $('.search') + @optsEl = @wrap.find('.search-autocomplete-opts') @autocompletePath = @optsEl.data('autocomplete-path') @projectId = @optsEl.data('autocomplete-project-id') || '' @projectRef = @optsEl.data('autocomplete-project-ref') || '' + } = opts - @keyCode = - ESCAPE: 27 - BACKSPACE: 8 - TAB: 9 - ENTER: 13 + # Dropdown Element + @dropdown = @wrap.find('.dropdown') @locationBadgeEl = @$('.search-location-badge') @locationText = @$('.location-text') + @scopeInputEl = @$('#scope') @searchInput = @$('.search-input') @projectInputEl = @$('#search_project_id') @groupInputEl = @$('#group_id') @@ -25,9 +32,7 @@ class @SearchAutocomplete @saveOriginalState() - # If there's no location badge - if !@locationBadgeEl.children().length - @createAutocomplete() + @searchInput.addClass('disabled') @bindEvents() @@ -37,6 +42,118 @@ class @SearchAutocomplete saveOriginalState: -> @originalState = @serializeState() + serializeState: -> + { + # Search Criteria + project_id: @projectInputEl.val() + group_id: @groupInputEl.val() + search_code: @searchCodeInputEl.val() + repository_ref: @repositoryInputEl.val() + + # Location badge + _location: $.trim(@locationText.text()) + } + + bindEvents: -> + @searchInput.on 'keydown', @onSearchInputKeyDown + @searchInput.on 'focus', @onSearchInputFocus + @searchInput.on 'blur', @onSearchInputBlur + + enableAutocomplete: -> + self = @ + @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef + dropdownMenu = self.dropdown.find('.dropdown-menu') + + @searchInput.glDropdown( + filterInputBlur: false + filterable: true + filterRemote: true + highlight: true + filterInput: 'input#search' + search: + fields: ['text'] + data: (term, callback) -> + $.ajax + url: self.autocompletePath + self.query + data: + term: term + beforeSend: -> + # dropdownMenu.addClass 'is-loading' + success: (response) -> + data = [] + + # Save groups ordering according to server response + groupNames = _.unique(_.pluck(response, 'category')) + + # Group results by category name + groups = _.groupBy response, (item) -> + item.category + + # List results + for groupName in groupNames + + # Add group header before list each group + data.push + header: groupName + + # List group + for item in groups[groupName] + data.push + text: item.label + url: item.url + + callback(data) + complete: -> + # dropdownMenu.removeClass 'is-loading' + + ) + + @dropdown.addClass('open') + @searchInput.removeClass('disabled') + @autocomplete = true; + + onDropdownOpen: (e) => + @dropdown.dropdown('toggle') + + onSearchInputKeyDown: (e) => + # Remove tag when pressing backspace and input search is empty + if e.keyCode is KEYCODE.BACKSPACE and e.currentTarget.value is '' + @removeLocationBadge() + @searchInput.focus() + + else if e.keyCode is KEYCODE.ESCAPE + @searchInput.val('') + @restoreOriginalState() + else + # Create new autocomplete if it hasn't been created yet and there's no badge + if @autocomplete is undefined + if !@badgePresent() + @enableAutocomplete() + else + # There's a badge + if @badgePresent() + @disableAutocomplete() + + onSearchInputFocus: => + @wrap.addClass('search-active') + + onSearchInputBlur: => + @wrap.removeClass('search-active') + + # If input is blank then restore state + if @searchInput.val() is '' + @restoreOriginalState() + + addLocationBadge: (item) -> + category = if item.category? then "#{item.category}: " else '' + value = if item.value? then item.value else '' + + html = " + #{category}#{value} + x + " + @locationBadgeEl.html(html) + restoreOriginalState: -> inputs = Object.keys @originalState @@ -51,122 +168,14 @@ class @SearchAutocomplete value: @originalState._location ) - serializeState: -> - { - # Search Criteria - project_id: @projectInputEl.val() - group_id: @groupInputEl.val() - search_code: @searchCodeInputEl.val() - repository_ref: @repositoryInputEl.val() + @dropdown.removeClass 'open' - # Location badge - _location: $.trim(@locationText.text()) - } + # Only add class if there's a badge + if @badgePresent() + @searchInput.addClass 'disabled' - createAutocomplete: -> - @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef - - @searchInput.catcomplete - appendTo: 'form.navbar-form' - source: @autocompletePath + @query - minLength: 1 - maxShowItems: 15 - position: - # { my: "left top", at: "left bottom", collision: "none" } - my: "left-10 top+9" - at: "left bottom" - collision: "none" - close: (e) -> - e.preventDefault() - - select: (event, ui) => - # Pressing enter choses an alternative - if event.keyCode is @keyCode.ENTER - @goToResult(ui.item) - else - # Pressing tab sets the location - if event.keyCode is @keyCode.TAB and ui.item.location? - @setLocationBadge(ui.item) - @searchInput - .val('') # remove selected value from input - .focus() - else - # If option is not a location go to page - @goToResult(ui.item) - - # Return false to avoid focus on the next element - return false - - @autocomplete = @searchInput.data 'customCatcomplete' - - bindEvents: -> - @searchInput.on 'keydown', @onSearchInputKeyDown - @searchInput.on 'focus', @onSearchInputFocus - @searchInput.on 'blur', @onSearchInputBlur - @wrap.on 'click', '.remove-badge', @onRemoveLocationBadgeClick - - onRemoveLocationBadgeClick: (e) => - e.preventDefault() - @removeLocationBadge() - @searchInput.focus() - - onSearchInputKeyDown: (e) => - # Remove tag when pressing backspace and input search is empty - if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is '' - @removeLocationBadge() - # @destroyAutocomplete() - @searchInput.focus() - else if e.keyCode is @keyCode.ESCAPE - @restoreOriginalState() - else - # Create new autocomplete if hasn't been created yet and there's no badge - if @autocomplete is undefined - if !@locationBadgeEl.children().length - @createAutocomplete() - else - # There's a badge - if @locationBadgeEl.children().length - @destroyAutocomplete() - - onSearchInputFocus: => - @wrap.addClass('search-active') - - onSearchInputBlur: => - @wrap.removeClass('search-active') - - # If input is blank then restore state - @restoreOriginalState() if @searchInput.val() is '' - - addLocationBadge: (item) -> - category = if item.category? then "#{item.category}: " else '' - value = if item.value? then item.value else '' - - html = " - #{category}#{value} - x - " - @locationBadgeEl.html(html) - - setLocationBadge: (item) -> - @addLocationBadge(item) - - # Reset input states - @resetSearchState() - - switch item.location - when 'projects' - @projectInputEl.val(item.id) - # @searchCodeInputEl.val('true') # TODO: always true for projects? - # @repositoryInputEl.val('master') # TODO: always master? - - when 'groups' - @groupInputEl.val(item.id) - - removeLocationBadge: -> - @locationBadgeEl.empty() - - # Reset state - @resetSearchState() + badgePresent: -> + @locationBadgeEl.children().length resetSearchState: -> # Remove scope @@ -184,10 +193,13 @@ class @SearchAutocomplete # Remove repository ref @repositoryInputEl.val('') - goToResult: (result) -> - location.href = result.url + removeLocationBadge: -> + @locationBadgeEl.empty() - destroyAutocomplete: -> - @autocomplete.destroy() if @autocomplete isnt undefined - @searchInput.attr('autocomplete', 'off') + # Reset state + @resetSearchState() + + disableAutocomplete: -> + if @autocomplete isnt undefined + @searchInput.addClass('disabled') @autocomplete = undefined diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 76b4cea4778..85a6f4b8b55 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -49,12 +49,6 @@ margin: 0; } } - - .ui-autocomplete-category { - text-transform: uppercase; - font-size: 11px; - color: #7f8fa4; - } } .ui-state-default { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index bc660985ecb..ff32bca98dc 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -21,7 +21,6 @@ } } - .search { margin-right: 10px; margin-left: 10px; @@ -51,7 +50,6 @@ padding: 4px; width: 350px; line-height: 24px; - overflow: hidden; } .location-text { @@ -69,7 +67,7 @@ padding: 0; margin-left: 5px; line-height: 25px; - width: 100%; + width: 98%; } .location-badge { @@ -89,7 +87,7 @@ } .search-location-badge, .search-input-wrap { - // Fallback if flex is not supported + // Fallback if flexbox is not supported display: inline-block; } @@ -103,6 +101,7 @@ position: absolute; right: 5px; color: #E7E9ED; + top: 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -114,9 +113,9 @@ } } - .ui-autocomplete-loading + .search-icon { - @extend .fa-spinner; - @extend .fa-spin; + .dropdown-header { + text-transform: uppercase; + font-size: 11px; } } } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index a004908fb6f..f051e7a1867 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -4,7 +4,11 @@ .search-location-badge = render 'shared/location_badge' .search-input-wrap - = search_field_tag "search", nil, placeholder: 'Search', class: "search-input", spellcheck: false, tabindex: "1", autocomplete: 'off' + .dropdown{ data: {url: search_autocomplete_path } } + = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } + .dropdown-menu.dropdown-select + = dropdown_content + = dropdown_loading %i.search-icon = hidden_field_tag :group_id, @group.try(:id) From 2925fc96a2c8f2fa6fa8e8f09565be998ef305ae Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 14 Mar 2016 16:23:41 -0500 Subject: [PATCH 018/264] Delete unused file --- .../lib/category_autocomplete.js.coffee | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 app/assets/javascripts/lib/category_autocomplete.js.coffee diff --git a/app/assets/javascripts/lib/category_autocomplete.js.coffee b/app/assets/javascripts/lib/category_autocomplete.js.coffee deleted file mode 100644 index c85fabbcd5b..00000000000 --- a/app/assets/javascripts/lib/category_autocomplete.js.coffee +++ /dev/null @@ -1,49 +0,0 @@ -$.widget( "custom.catcomplete", $.ui.autocomplete, - _create: -> - @_super(); - @widget().menu("option", "items", "> :not(.ui-autocomplete-category)") - - _renderMenu: (ul, items) -> - currentCategory = '' - $.each items, (index, item) => - if item.category isnt currentCategory - ul.append("
  • #{item.category}
  • ") - currentCategory = item.category - - li = @_renderItemData(ul, item) - - if item.category? - li.attr('aria-label', item.category + " : " + item.label) - - _renderItem: (ul, item) -> - # Highlight occurrences - item.label = item.label.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + $.ui.autocomplete.escapeRegex(this.term) + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); - - return $( "
  • " ) - .data( "item.autocomplete", item ) - .append( "#{item.label}" ) - .appendTo( ul ); - - _resizeMenu: -> - if (isNaN(this.options.maxShowItems)) - return - - ul = this.menu.element.css(overflowX: '', overflowY: '', width: '', maxHeight: '') - - lis = ul.children('li').css('whiteSpace', 'nowrap'); - - if (lis.length > this.options.maxShowItems) - ulW = ul.prop('clientWidth') - - ul.css( - overflowX: 'hidden' - overflowY: 'auto' - maxHeight: lis.eq(0).outerHeight() * this.options.maxShowItems + 1 - ) - - barW = ulW - ul.prop('clientWidth'); - ul.width('+=' + barW); - - # Original code from jquery.ui.autocomplete.js _resizeMenu() - ul.outerWidth(Math.max(ul.outerWidth() + 1, this.element.outerWidth())); - ) From 37440200df4f0618e9fd526bba7d3d1cddae1cab Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 14 Mar 2016 22:04:22 -0500 Subject: [PATCH 019/264] Fixes failing spec --- app/assets/javascripts/gl_dropdown.js.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 0684e7852fa..c579657d4d2 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -237,13 +237,12 @@ class GitLabDropdown html = @options.renderRow(data) else selected = if @options.isSelected then @options.isSelected(data) else false - url = if @options.url then @options.url(data) else "#" # Set URL if @options.url? url = @options.url(data) else - url = if data.url? then data.url else '' + url = if data.url? then data.url else '#' # Set Text if @options.text? From 91880e13df19ed312bfa0a2e06743dd8a71aa1ad Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 18:54:06 -0500 Subject: [PATCH 020/264] initial ajax build --- .../merge_request_widget.js.coffee | 24 ++++++++++++++----- .../projects/merge_requests_controller.rb | 3 ++- .../merge_requests/widget/_show.html.haml | 3 ++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 738ffc8343b..98f200f9b8a 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -9,6 +9,7 @@ class @MergeRequestWidget # constructor: (@opts) -> modal = $('#modal_merge_info').modal(show: false) + @getBuildStatus() mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -30,13 +31,24 @@ class @MergeRequestWidget $.get @opts.url_to_automerge_check, (data) -> $('.mr-state-widget').replaceWith(data) + getBuildStatus: -> + urlToCiCheck = @opts.url_to_ci_check + ciEnabled = @opts.ci_enable + console.log(ciEnabled) + setInterval (-> + if ciEnabled + $.getJSON urlToCiCheck, (data) -> + console.log("data",data); + return + return + ), 5000 + getCiStatus: -> - if @opts.ci_enable - $.get @opts.url_to_ci_check, (data) => - this.showCiState data.status - if data.coverage - this.showCiCoverage data.coverage - , 'json' + $.get @opts.url_to_ci_check, (data) => + this.showCiState data.status + if data.coverage + this.showCiCoverage data.coverage + , 'json' showCiState: (state) -> $('.ci_widget').hide() diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 61b82c9db46..861ae7ee2f7 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -228,7 +228,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController response = { status: status, - coverage: coverage + coverage: coverage, + ci_status: @merge_request.ci_commit.status } render json: response diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a489d4f9b24..a86677c23ad 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -15,6 +15,7 @@ check_enable: #{@merge_request.unchecked? ? "true" : "false"}, url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_enable: #{@project.ci_service ? "true" : "false"}, - current_status: "#{@merge_request.gitlab_merge_status}", + current_status: "#{@merge_request.gitlab_merge_status}" }); + var cici = "#{@project}" From 51ceb3802f07d82fe9fa606382cf2f1074e1cfb5 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 21 Jan 2016 07:24:02 -0500 Subject: [PATCH 021/264] Adds JSON callback, which is currently not working. --- .../javascripts/merge_request_widget.js.coffee | 12 +++++------- .../projects/merge_requests_controller.rb | 11 +++++++++-- .../projects/merge_requests/widget/_show.html.haml | 2 +- config/routes.rb | 1 + 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 98f200f9b8a..b1daa1f34eb 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -33,15 +33,13 @@ class @MergeRequestWidget getBuildStatus: -> urlToCiCheck = @opts.url_to_ci_check - ciEnabled = @opts.ci_enable - console.log(ciEnabled) + console.log('checking') setInterval (-> - if ciEnabled - $.getJSON urlToCiCheck, (data) -> - console.log("data",data); - return + $.getJSON urlToCiCheck, (data) -> + console.log("data",data); return - ), 5000 + return + ), 5000 getCiStatus: -> $.get @opts.url_to_ci_check, (data) => diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 861ae7ee2f7..259e25c91ab 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,6 +218,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def st + @ci_commit = @merge_request.ci_commit + @statuses = @ci_commit.statuses if @ci_commit + render json: { + statuses: @statuses + } + end + def ci_status ci_service = @merge_request.source_project.ci_service status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) @@ -228,8 +236,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController response = { status: status, - coverage: coverage, - ci_status: @merge_request.ci_commit.status + coverage: coverage } render json: response diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index a86677c23ad..268171fde08 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -13,7 +13,7 @@ merge_request_widget = new MergeRequestWidget({ url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + url_to_ci_check: "#{st_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_enable: #{@project.ci_service ? "true" : "false"}, current_status: "#{@merge_request.gitlab_merge_status}" }); diff --git a/config/routes.rb b/config/routes.rb index 2ae282f48a6..312d1ba35ac 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -620,6 +620,7 @@ Rails.application.routes.draw do post :merge post :cancel_merge_when_build_succeeds get :ci_status + get :st post :toggle_subscription end From f7e2109905ba21c4ca61e0ab74da208d18b6adeb Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Mon, 25 Jan 2016 16:20:24 -0500 Subject: [PATCH 022/264] Adds notifications API to MR page. When a build status changes a notification will popup. Fixes #10851 --- app/assets/javascripts/lib/notify.js.coffee | 27 +++++++++++++ .../merge_request_widget.js.coffee | 39 +++++++++++++++++-- .../projects/merge_requests_controller.rb | 28 +++++++------ .../merge_requests/widget/_heading.html.haml | 3 +- .../merge_requests/widget/_show.html.haml | 22 +++++++---- config/routes.rb | 1 - 6 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 app/assets/javascripts/lib/notify.js.coffee diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee new file mode 100644 index 00000000000..26924d87d68 --- /dev/null +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -0,0 +1,27 @@ +# Written by Jacob Schatz @jakecodes + +((w) -> + notifyMe = (message,body) -> + notification = undefined + opts = + body: body + icon: "#{document.location.origin}/assets/gitlab_logo.png" + # Let's check if the browser supports notifications + if !('Notification' of window) + # do nothing + else if Notification.permission == 'granted' + # If it's okay let's create a notification + notification = new Notification(message, opts) + else if Notification.permission != 'denied' + Notification.requestPermission (permission) -> + # If the user accepts, let's create a notification + if permission == 'granted' + notification = new Notification(message, opts) + return + return + + w.notify = notifyMe + return +) window + +Notification.requestPermission() \ No newline at end of file diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index b1daa1f34eb..4e422763543 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -10,6 +10,8 @@ class @MergeRequestWidget constructor: (@opts) -> modal = $('#modal_merge_info').modal(show: false) @getBuildStatus() + # clear the build poller + $(document).on 'page:fetch', (e) => clearInterval(@fetchBuildStatusInterval) mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -31,12 +33,43 @@ class @MergeRequestWidget $.get @opts.url_to_automerge_check, (data) -> $('.mr-state-widget').replaceWith(data) + ciIconForStatus: (status) -> + icon = undefined + switch status + when 'success' + icon = 'check' + when 'failed' + icon = 'close' + when 'running' or 'pending' + icon = 'clock-o' + else + icon = 'circle' + 'fa fa-' + icon + ' fa-fw' + + ciLabelForStatus: (status) -> + if status == 'success' + 'passed' + else + status + getBuildStatus: -> urlToCiCheck = @opts.url_to_ci_check - console.log('checking') - setInterval (-> + _this = @ + @fetchBuildStatusInterval = setInterval (-> $.getJSON urlToCiCheck, (data) -> - console.log("data",data); + if data.status isnt _this.opts.current_status + notify("Build #{_this.ciLabelForStatus(data.status)}", + _this.opts.ci_message.replace('{{status}}', + _this.ciLabelForStatus(data.status))); + _this.opts.current_status = data.status + $('.mr-widget-heading i') + .removeClass() + .addClass(_this.ciIconForStatus(data.status)); + $('.mr-widget-heading .ci_widget') + .removeClass() + .addClass("ci_widget ci-#{data.status}"); + $('.mr-widget-heading span.ci-status-label') + .text(_this.ciLabelForStatus(data.status)) return return ), 5000 diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 259e25c91ab..987b3e1c5b6 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,28 +218,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end - def st - @ci_commit = @merge_request.ci_commit - @statuses = @ci_commit.statuses if @ci_commit - render json: { - statuses: @statuses - } - end - def ci_status - ci_service = @merge_request.source_project.ci_service - status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) + ci_commit = @merge_request.ci_commit + if ci_commit + status = ci_commit.try(:status) + coverage = ci_commit.try(:coverage) + else + ci_service = @merge_request.source_project.ci_service + status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + if ci_service.respond_to?(:commit_coverage) + coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch) + end end response = { - status: status, - coverage: coverage + status: status || :not_found, + coverage: coverage || :not_found } - render json: response + render json: response, status: 200 end protected diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index b05ab869215..ccb2f9fa77e 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -4,7 +4,8 @@ = ci_status_icon(@ci_commit) %span Build - = ci_status_label(@ci_commit) + %span.ci-status-label + = ci_status_label(@ci_commit) for = succeed "." do = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 268171fde08..73ec56d170a 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -9,13 +9,21 @@ :javascript var merge_request_widget; - - merge_request_widget = new MergeRequestWidget({ + var opts = { url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{st_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", - ci_enable: #{@project.ci_service ? "true" : "false"}, - current_status: "#{@merge_request.gitlab_merge_status}" - }); - var cici = "#{@project}" + url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_enable: #{@project.ci_service ? "true" : "false"} + }; +- if @merge_request.ci_commit + :javascript + opts.current_status = "#{@merge_request.ci_commit.try(:status)}"; + opts.ci_message = "Build {{status}} for #{@merge_request.ci_commit.sha}"; +- else + :javascript + opts.current_status = "#{@merge_request.source_project.ci_service.commit_status(@merge_request.last_commit.sha, merge_request.source_branch) if @merge_request.source_project.ci_service}"; + opts.ci_message = "Build {{status}} for #{@merge_request.last_commit.sha}"; + +:javascript + merge_request_widget = new MergeRequestWidget(opts); \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 312d1ba35ac..2ae282f48a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -620,7 +620,6 @@ Rails.application.routes.draw do post :merge post :cancel_merge_when_build_succeeds get :ci_status - get :st post :toggle_subscription end From e9c5e31281e4ad3c84083c39d5e50087ce909144 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 3 Mar 2016 18:02:18 -0500 Subject: [PATCH 023/264] Add icon as a opt for notifier --- app/assets/javascripts/lib/notify.js.coffee | 6 +++--- app/views/projects/merge_requests/widget/_show.html.haml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee index 26924d87d68..2cb4481fa00 100644 --- a/app/assets/javascripts/lib/notify.js.coffee +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -1,11 +1,11 @@ -# Written by Jacob Schatz @jakecodes +# Written by GitLab @gitlab ((w) -> - notifyMe = (message,body) -> + notifyMe = (message,body, icon) -> notification = undefined opts = body: body - icon: "#{document.location.origin}/assets/gitlab_logo.png" + icon: icon # Let's check if the browser supports notifications if !('Notification' of window) # do nothing diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 73ec56d170a..ac7daa54ebe 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -13,6 +13,7 @@ url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + gitlab_icon: #{asset_path "gitlab_logo.png"}, ci_enable: #{@project.ci_service ? "true" : "false"} }; From b2f2df3b38a5ec5fe96a018309f0caf511f9e1d0 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 3 Mar 2016 19:19:00 -0500 Subject: [PATCH 024/264] Add page reload as a temporary boring solution --- app/assets/javascripts/merge_request_widget.js.coffee | 11 +++++++++-- .../projects/merge_requests/widget/_show.html.haml | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 4e422763543..bebedeca28f 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -11,7 +11,9 @@ class @MergeRequestWidget modal = $('#modal_merge_info').modal(show: false) @getBuildStatus() # clear the build poller - $(document).on 'page:fetch', (e) => clearInterval(@fetchBuildStatusInterval) + $(document) + .off 'page:fetch' + .on 'page:fetch', (e) => clearInterval(@fetchBuildStatusInterval) mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -60,7 +62,12 @@ class @MergeRequestWidget if data.status isnt _this.opts.current_status notify("Build #{_this.ciLabelForStatus(data.status)}", _this.opts.ci_message.replace('{{status}}', - _this.ciLabelForStatus(data.status))); + _this.ciLabelForStatus(data.status)), + _this.opts.gitlab_icon) + setTimeout (-> + window.location.reload() + return + ), 2000 _this.opts.current_status = data.status $('.mr-widget-heading i') .removeClass() diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index ac7daa54ebe..fd45b1b9789 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -13,7 +13,7 @@ url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", - gitlab_icon: #{asset_path "gitlab_logo.png"}, + gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_enable: #{@project.ci_service ? "true" : "false"} }; From fcc0f7c68ecdb17c3ee6173515663a7c2854f44c Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 3 Mar 2016 19:40:51 -0500 Subject: [PATCH 025/264] Remove repeated build listing --- app/assets/javascripts/merge_request_widget.js.coffee | 5 +---- app/views/projects/merge_requests/widget/_show.html.haml | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index bebedeca28f..97198e14248 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -11,9 +11,6 @@ class @MergeRequestWidget modal = $('#modal_merge_info').modal(show: false) @getBuildStatus() # clear the build poller - $(document) - .off 'page:fetch' - .on 'page:fetch', (e) => clearInterval(@fetchBuildStatusInterval) mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -65,7 +62,7 @@ class @MergeRequestWidget _this.ciLabelForStatus(data.status)), _this.opts.gitlab_icon) setTimeout (-> - window.location.reload() + Turbolinks.visit(location.href) return ), 2000 _this.opts.current_status = data.status diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index fd45b1b9789..dbc6d6c3f90 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -27,4 +27,6 @@ opts.ci_message = "Build {{status}} for #{@merge_request.last_commit.sha}"; :javascript - merge_request_widget = new MergeRequestWidget(opts); \ No newline at end of file + if(typeof merge_request_widget === 'undefined') { + merge_request_widget = new MergeRequestWidget(opts); + } \ No newline at end of file From 00deaaafb1149d78a019d96b02ca2e6279d39f25 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Thu, 3 Mar 2016 20:25:42 -0500 Subject: [PATCH 026/264] removing ci_enable --- app/assets/javascripts/merge_request_widget.js.coffee | 1 - app/views/projects/merge_requests/widget/_show.html.haml | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 97198e14248..27537d72661 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -4,7 +4,6 @@ class @MergeRequestWidget # check_enable - Boolean, whether to check automerge status # url_to_automerge_check - String, URL to use to check automerge status # current_status - String, current automerge status - # ci_enable - Boolean, whether a CI service is enabled # url_to_ci_check - String, URL to use to check CI status # constructor: (@opts) -> diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index dbc6d6c3f90..9537eda5aa5 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -13,8 +13,7 @@ url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", - gitlab_icon: "#{asset_path 'gitlab_logo.png'}", - ci_enable: #{@project.ci_service ? "true" : "false"} + gitlab_icon: "#{asset_path 'gitlab_logo.png'}" }; - if @merge_request.ci_commit From e33e0de24da8994c32ce093883003d31cef7c56e Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 4 Mar 2016 15:57:32 -0500 Subject: [PATCH 027/264] Checks if Notification API exists before requesting permission. --- app/assets/javascripts/lib/notify.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee index 2cb4481fa00..6b9fb9deb3b 100644 --- a/app/assets/javascripts/lib/notify.js.coffee +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -24,4 +24,5 @@ return ) window -Notification.requestPermission() \ No newline at end of file +if 'Notification' of window + Notification.requestPermission() \ No newline at end of file From 1a482bfbc21eca3c7526cc367b86174b77e0d617 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 4 Mar 2016 17:42:32 -0500 Subject: [PATCH 028/264] Removes name from file Changes `:not_found` to `nil` --- app/assets/javascripts/lib/notify.js.coffee | 2 -- app/controllers/projects/merge_requests_controller.rb | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee index 6b9fb9deb3b..f28fe8bbc63 100644 --- a/app/assets/javascripts/lib/notify.js.coffee +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -1,5 +1,3 @@ -# Written by GitLab @gitlab - ((w) -> notifyMe = (message,body, icon) -> notification = undefined diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 987b3e1c5b6..e57471deccc 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -233,8 +233,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end response = { - status: status || :not_found, - coverage: coverage || :not_found + status: status || nil, + coverage: coverage || nil } render json: response, status: 200 From c1c786fe65be89da63db7d9e5e6523ba549f2f8a Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 4 Mar 2016 18:22:39 -0500 Subject: [PATCH 029/264] Using status from ajax call. Removing icon changes because refresh. --- .../merge_request_widget.js.coffee | 33 +++++++------------ .../merge_requests/widget/_show.html.haml | 5 ++- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 27537d72661..168de57288b 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -3,12 +3,14 @@ class @MergeRequestWidget # # check_enable - Boolean, whether to check automerge status # url_to_automerge_check - String, URL to use to check automerge status - # current_status - String, current automerge status # url_to_ci_check - String, URL to use to check CI status # + constructor: (@opts) -> + @first = true modal = $('#modal_merge_info').modal(show: false) @getBuildStatus() + @readyForCICheck = true # clear the build poller mergeInProgress: (deleteSourceBranch = false)-> @@ -31,19 +33,6 @@ class @MergeRequestWidget $.get @opts.url_to_automerge_check, (data) -> $('.mr-state-widget').replaceWith(data) - ciIconForStatus: (status) -> - icon = undefined - switch status - when 'success' - icon = 'check' - when 'failed' - icon = 'close' - when 'running' or 'pending' - icon = 'clock-o' - else - icon = 'circle' - 'fa fa-' + icon + ' fa-fw' - ciLabelForStatus: (status) -> if status == 'success' 'passed' @@ -54,7 +43,13 @@ class @MergeRequestWidget urlToCiCheck = @opts.url_to_ci_check _this = @ @fetchBuildStatusInterval = setInterval (-> + if not _this.readyForCICheck + return; $.getJSON urlToCiCheck, (data) -> + _this.readyForCICheck = true + if _this.first + _this.first = false + _this.opts.current_status = data.status if data.status isnt _this.opts.current_status notify("Build #{_this.ciLabelForStatus(data.status)}", _this.opts.ci_message.replace('{{status}}', @@ -65,16 +60,10 @@ class @MergeRequestWidget return ), 2000 _this.opts.current_status = data.status - $('.mr-widget-heading i') - .removeClass() - .addClass(_this.ciIconForStatus(data.status)); - $('.mr-widget-heading .ci_widget') - .removeClass() - .addClass("ci_widget ci-#{data.status}"); - $('.mr-widget-heading span.ci-status-label') - .text(_this.ciLabelForStatus(data.status)) return + _this.readyForCICheck = false return + ), 5000 getCiStatus: -> diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 9537eda5aa5..b5591416a31 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -13,16 +13,15 @@ url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", - gitlab_icon: "#{asset_path 'gitlab_logo.png'}" + gitlab_icon: "#{asset_path 'gitlab_logo.png'}", + current_status: "" }; - if @merge_request.ci_commit :javascript - opts.current_status = "#{@merge_request.ci_commit.try(:status)}"; opts.ci_message = "Build {{status}} for #{@merge_request.ci_commit.sha}"; - else :javascript - opts.current_status = "#{@merge_request.source_project.ci_service.commit_status(@merge_request.last_commit.sha, merge_request.source_branch) if @merge_request.source_project.ci_service}"; opts.ci_message = "Build {{status}} for #{@merge_request.last_commit.sha}"; :javascript From b0e2e2e06ed38d8a23e8f834d389baa18a7a885e Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 9 Mar 2016 16:09:47 -0500 Subject: [PATCH 030/264] Fix code style issues. --- app/controllers/projects/merge_requests_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e57471deccc..e40ec38fbff 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -233,11 +233,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController end response = { - status: status || nil, - coverage: coverage || nil + status: status, + coverage: coverage } - render json: response, status: 200 + render json: response end protected From fcba25515321f57e36b9a8f2156d6b72eafb4c14 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 16 Mar 2016 14:31:35 +0000 Subject: [PATCH 031/264] Commit SHA comes from JSON Removed page refresh - instead clicking takes to the builds tab --- app/assets/javascripts/lib/notify.js.coffee | 18 ++++-- .../merge_request_widget.js.coffee | 56 ++++++++++--------- .../projects/merge_requests_controller.rb | 3 +- .../merge_requests/widget/_show.html.haml | 15 ++--- 4 files changed, 49 insertions(+), 43 deletions(-) diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee index f28fe8bbc63..bd409faba95 100644 --- a/app/assets/javascripts/lib/notify.js.coffee +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -1,7 +1,11 @@ ((w) -> - notifyMe = (message,body, icon) -> + notifyPermissions = -> + if 'Notification' of window + Notification.requestPermission() + + notifyMe = (message, body, icon, onclick) -> notification = undefined - opts = + opts = body: body icon: icon # Let's check if the browser supports notifications @@ -10,17 +14,21 @@ else if Notification.permission == 'granted' # If it's okay let's create a notification notification = new Notification(message, opts) + + if onclick + notification.onclick = onclick else if Notification.permission != 'denied' Notification.requestPermission (permission) -> # If the user accepts, let's create a notification if permission == 'granted' notification = new Notification(message, opts) + + if onclick + notification.onclick = onclick return return w.notify = notifyMe + w.notifyPermissions = notifyPermissions return ) window - -if 'Notification' of window - Notification.requestPermission() \ No newline at end of file diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 168de57288b..9afb6a0ce86 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -7,9 +7,10 @@ class @MergeRequestWidget # constructor: (@opts) -> - @first = true + @firstCICheck = true modal = $('#modal_merge_info').modal(show: false) - @getBuildStatus() + @getCIStatus() + notifyPermissions() @readyForCICheck = true # clear the build poller @@ -39,31 +40,34 @@ class @MergeRequestWidget else status - getBuildStatus: -> - urlToCiCheck = @opts.url_to_ci_check - _this = @ - @fetchBuildStatusInterval = setInterval (-> - if not _this.readyForCICheck - return; - $.getJSON urlToCiCheck, (data) -> - _this.readyForCICheck = true - if _this.first - _this.first = false - _this.opts.current_status = data.status - if data.status isnt _this.opts.current_status - notify("Build #{_this.ciLabelForStatus(data.status)}", - _this.opts.ci_message.replace('{{status}}', - _this.ciLabelForStatus(data.status)), - _this.opts.gitlab_icon) - setTimeout (-> - Turbolinks.visit(location.href) - return - ), 2000 - _this.opts.current_status = data.status - return - _this.readyForCICheck = false - return + getCIStatus: -> + urlToCICheck = @opts.url_to_ci_check + @fetchBuildStatusInterval = setInterval ( => + return if not @readyForCICheck + $.getJSON urlToCICheck, (data) => + @readyForCICheck = true + + if @firstCICheck + @firstCICheck = false + @opts.current_status = data.status + + if data.status isnt @opts.current_status + message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) + message = message.replace('{{sha}}', data.sha) + + notify( + "Build #{_this.ciLabelForStatus(data.status)}", + message, + @opts.gitlab_icon, + -> + @close() + Turbolinks.visit "#{window.location.pathname}/builds" + ) + + @opts.current_status = data.status + + @readyForCICheck = false ), 5000 getCiStatus: -> diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e40ec38fbff..2cc94596d2b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -221,7 +221,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def ci_status ci_commit = @merge_request.ci_commit if ci_commit - status = ci_commit.try(:status) + status = ci_commit.status coverage = ci_commit.try(:coverage) else ci_service = @merge_request.source_project.ci_service @@ -233,6 +233,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end response = { + sha: merge_request.last_commit.sha, status: status, coverage: coverage } diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index b5591416a31..8193bb4d180 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -14,17 +14,10 @@ check_enable: #{@merge_request.unchecked? ? "true" : "false"}, url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", - current_status: "" + current_status: "", + ci_message: "Build {{status}} for {{sha}}" }; - -- if @merge_request.ci_commit - :javascript - opts.ci_message = "Build {{status}} for #{@merge_request.ci_commit.sha}"; -- else - :javascript - opts.ci_message = "Build {{status}} for #{@merge_request.last_commit.sha}"; - -:javascript + if(typeof merge_request_widget === 'undefined') { merge_request_widget = new MergeRequestWidget(opts); - } \ No newline at end of file + } From 33aeaf6a9c926d269f090f3e4a9c048661b8078e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 16 Mar 2016 14:52:56 +0000 Subject: [PATCH 032/264] Merge request title is in the notification Short commit instead of long commit sha --- app/assets/javascripts/merge_request_widget.js.coffee | 5 ++++- app/controllers/projects/merge_requests_controller.rb | 3 ++- app/views/projects/merge_requests/widget/_show.html.haml | 5 +++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 9afb6a0ce86..b74b8c21fd5 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -42,6 +42,8 @@ class @MergeRequestWidget getCIStatus: -> urlToCICheck = @opts.url_to_ci_check + _this = @ + @fetchBuildStatusInterval = setInterval ( => return if not @readyForCICheck @@ -55,6 +57,7 @@ class @MergeRequestWidget if data.status isnt @opts.current_status message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) message = message.replace('{{sha}}', data.sha) + message = message.replace('{{title}}', data.title) notify( "Build #{_this.ciLabelForStatus(data.status)}", @@ -62,7 +65,7 @@ class @MergeRequestWidget @opts.gitlab_icon, -> @close() - Turbolinks.visit "#{window.location.pathname}/builds" + Turbolinks.visit _this.opts.builds_path ) @opts.current_status = data.status diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2cc94596d2b..728d743045f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -233,7 +233,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end response = { - sha: merge_request.last_commit.sha, + title: merge_request.title, + sha: merge_request.last_commit_short_sha, status: status, coverage: coverage } diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 8193bb4d180..6507c534a02 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -15,9 +15,10 @@ url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", current_status: "", - ci_message: "Build {{status}} for {{sha}}" + ci_message: "Build {{status}} for {{title}}\n{{sha}}", + builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; - + if(typeof merge_request_widget === 'undefined') { merge_request_widget = new MergeRequestWidget(opts); } From 3d6573fd7149fc747fcfb6f92a24dff232ab6cad Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 18 Mar 2016 11:08:03 +0000 Subject: [PATCH 033/264] Updated to fix issues risen during feedback Correctly updates the on-screen CI text feedback --- app/assets/javascripts/lib/notify.js.coffee | 20 +++---- .../merge_request_widget.js.coffee | 36 ++++++------ .../merge_requests/widget/_heading.html.haml | 55 +++++++------------ .../merge_requests/widget/_show.html.haml | 9 +-- 4 files changed, 53 insertions(+), 67 deletions(-) diff --git a/app/assets/javascripts/lib/notify.js.coffee b/app/assets/javascripts/lib/notify.js.coffee index bd409faba95..3f9ca39912c 100644 --- a/app/assets/javascripts/lib/notify.js.coffee +++ b/app/assets/javascripts/lib/notify.js.coffee @@ -1,10 +1,15 @@ ((w) -> + notificationGranted = (message, opts, onclick) -> + notification = new Notification(message, opts) + + if onclick + notification.onclick = onclick + notifyPermissions = -> if 'Notification' of window Notification.requestPermission() notifyMe = (message, body, icon, onclick) -> - notification = undefined opts = body: body icon: icon @@ -13,22 +18,13 @@ # do nothing else if Notification.permission == 'granted' # If it's okay let's create a notification - notification = new Notification(message, opts) - - if onclick - notification.onclick = onclick + notificationGranted message, opts, onclick else if Notification.permission != 'denied' Notification.requestPermission (permission) -> # If the user accepts, let's create a notification if permission == 'granted' - notification = new Notification(message, opts) - - if onclick - notification.onclick = onclick - return - return + notificationGranted message, opts, onclick w.notify = notifyMe w.notifyPermissions = notifyPermissions - return ) window diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index b74b8c21fd5..0bb95e92158 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -2,8 +2,8 @@ class @MergeRequestWidget # Initialize MergeRequestWidget behavior # # check_enable - Boolean, whether to check automerge status - # url_to_automerge_check - String, URL to use to check automerge status - # url_to_ci_check - String, URL to use to check CI status + # merge_check_url - String, URL to use to check automerge status + # ci_status_url - String, URL to use to check CI status # constructor: (@opts) -> @@ -31,7 +31,7 @@ class @MergeRequestWidget dataType: 'json' getMergeStatus: -> - $.get @opts.url_to_automerge_check, (data) -> + $.get @opts.merge_check_url, (data) -> $('.mr-state-widget').replaceWith(data) ciLabelForStatus: (status) -> @@ -41,26 +41,28 @@ class @MergeRequestWidget status getCIStatus: -> - urlToCICheck = @opts.url_to_ci_check _this = @ - @fetchBuildStatusInterval = setInterval ( => return if not @readyForCICheck - $.getJSON urlToCICheck, (data) => + $.getJSON @opts.ci_status_url, (data) => @readyForCICheck = true if @firstCICheck @firstCICheck = false - @opts.current_status = data.status + @opts.ci_status = data.status + + if data.status isnt @opts.ci_status + @showCIState data.status + if data.coverage + @showCICoverage data.coverage - if data.status isnt @opts.current_status message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) message = message.replace('{{sha}}', data.sha) message = message.replace('{{title}}', data.title) notify( - "Build #{_this.ciLabelForStatus(data.status)}", + "Build #{@ciLabelForStatus(data.status)}", message, @opts.gitlab_icon, -> @@ -68,19 +70,19 @@ class @MergeRequestWidget Turbolinks.visit _this.opts.builds_path ) - @opts.current_status = data.status + @opts.ci_status = data.status @readyForCICheck = false ), 5000 - getCiStatus: -> - $.get @opts.url_to_ci_check, (data) => - this.showCiState data.status + getCIState: -> + $('.ci-widget-fetching').show() + $.getJSON @opts.ci_status_url, (data) => + @showCIState data.status if data.coverage - this.showCiCoverage data.coverage - , 'json' + @showCICoverage data.coverage - showCiState: (state) -> + showCIState: (state) -> $('.ci_widget').hide() allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states @@ -94,7 +96,7 @@ class @MergeRequestWidget $('.ci_widget.ci-error').show() @setMergeButtonClass('btn-danger') - showCiCoverage: (coverage) -> + showCICoverage: (coverage) -> text = 'Coverage ' + coverage + '%' $('.ci_widget:visible .ci-coverage').text(text) diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index ccb2f9fa77e..2ee8e2de0e8 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,23 +1,12 @@ -- if @ci_commit - .mr-widget-heading - .ci_widget{class: "ci-#{@ci_commit.status}"} - = ci_status_icon(@ci_commit) - %span - Build - %span.ci-status-label - = ci_status_label(@ci_commit) - for - = succeed "." do - = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" - %span.ci-coverage - = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - -- elsif @merge_request.has_ci? - - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # Remove in later versions when services like Jenkins will set CI status via Commit status API +- if @ci_commit or @merge_request.has_ci? .mr-widget-heading + - if @merge_request.has_ci? + .ci_widget.ci-widget-fetching + = icon('spinner spin') + %span + Checking CI status for #{@merge_request.last_commit_short_sha}… - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless status == @ci_commit.status) } = ci_icon_for_status(status) %span CI build @@ -27,22 +16,20 @@ = succeed "." do = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage - - if details_path = ci_build_details_path(@merge_request) + - if details_path = builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink" + - if @merge_request.has_ci? + - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX + - # Remove in later versions when services like Jenkins will set CI status via Commit status API + .ci_widget.ci-not_found{style: "display:none"} + = icon("times-circle") + Could not find CI status for #{@merge_request.last_commit_short_sha}. - .ci_widget - = icon("spinner spin") - Checking CI status for #{@merge_request.last_commit_short_sha}… + .ci_widget.ci-error{style: "display:none"} + = icon("times-circle") + Could not connect to the CI server. Please check your settings and try again. - .ci_widget.ci-not_found{style: "display:none"} - = icon("times-circle") - Could not find CI status for #{@merge_request.last_commit_short_sha}. - - .ci_widget.ci-error{style: "display:none"} - = icon("times-circle") - Could not connect to the CI server. Please check your settings and try again. - - :javascript - $(function() { - merge_request_widget.getCiStatus(); - }); + :javascript + $(function() { + merge_request_widget.getCIState(); + }); diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 6507c534a02..2be06aebe6c 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -10,12 +10,13 @@ :javascript var merge_request_widget; var opts = { - url_to_automerge_check: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, - url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", - current_status: "", - ci_message: "Build {{status}} for {{title}}\n{{sha}}", + ci_status: "", + ci_message: "Build {{status}} for \"{{title}}\"", + ci_enable: #{@project.ci_service ? "true" : "false"}, builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; From 3b6e2a68f3bce709ee0b1df561b8e7a8bea359b8 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 18 Mar 2016 11:16:34 +0000 Subject: [PATCH 034/264] Removed modal hide --- app/assets/javascripts/merge_request_widget.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 0bb95e92158..877e85a12e9 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -8,7 +8,6 @@ class @MergeRequestWidget constructor: (@opts) -> @firstCICheck = true - modal = $('#modal_merge_info').modal(show: false) @getCIStatus() notifyPermissions() @readyForCICheck = true From c9e202c1d7cab4918e8b3789e9e6a553c30ead2a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 8 Mar 2016 02:56:43 -0500 Subject: [PATCH 035/264] Working version of autocomplete with categorized results --- app/assets/javascripts/dispatcher.js.coffee | 7 +- .../lib/category_autocomplete.js.coffee | 17 ++ .../javascripts/search_autocomplete.js.coffee | 169 +++++++++++++++++- app/helpers/search_helper.rb | 59 +++--- app/views/layouts/_search.html.haml | 13 +- app/views/shared/_location_badge.html.haml | 13 ++ 6 files changed, 229 insertions(+), 49 deletions(-) create mode 100644 app/assets/javascripts/lib/category_autocomplete.js.coffee create mode 100644 app/views/shared/_location_badge.html.haml diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index f5e1ca9860d..a022c207d08 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -152,9 +152,4 @@ class Dispatcher new Shortcuts() initSearch: -> - opts = $('.search-autocomplete-opts') - path = opts.data('autocomplete-path') - project_id = opts.data('autocomplete-project-id') - project_ref = opts.data('autocomplete-project-ref') - - new SearchAutocomplete(path, project_id, project_ref) + new SearchAutocomplete() diff --git a/app/assets/javascripts/lib/category_autocomplete.js.coffee b/app/assets/javascripts/lib/category_autocomplete.js.coffee new file mode 100644 index 00000000000..490032dc782 --- /dev/null +++ b/app/assets/javascripts/lib/category_autocomplete.js.coffee @@ -0,0 +1,17 @@ +$.widget( "custom.catcomplete", $.ui.autocomplete, + _create: -> + @_super(); + @widget().menu("option", "items", "> :not(.ui-autocomplete-category)") + + _renderMenu: (ul, items) -> + currentCategory = '' + $.each items, (index, item) => + if item.category isnt currentCategory + ul.append("
  • #{item.category}
  • ") + currentCategory = item.category + + li = @_renderItemData(ul, item) + + if item.category? + li.attr('aria-label', item.category + " : " + item.label) + ) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index c1801365266..df31b07910c 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,11 +1,164 @@ class @SearchAutocomplete - constructor: (search_autocomplete_path, project_id, project_ref) -> - project_id = '' unless project_id - project_ref = '' unless project_ref - query = "?project_id=" + project_id + "&project_ref=" + project_ref + constructor: (opts = {}) -> + { + @wrap = $('.search') + @optsEl = @wrap.find('.search-autocomplete-opts') + @autocompletePath = @optsEl.data('autocomplete-path') + @projectId = @optsEl.data('autocomplete-project-id') || '' + @projectRef = @optsEl.data('autocomplete-project-ref') || '' + } = opts - $("#search").autocomplete - source: search_autocomplete_path + query + @keyCode = + ESCAPE: 27 + BACKSPACE: 8 + TAB: 9 + ENTER: 13 + + @locationBadgeEl = @$('.search-location-badge') + @locationText = @$('.location-text') + @searchInput = @$('.search-input') + @projectInputEl = @$('#project_id') + @groupInputEl = @$('#group_id') + @searchCodeInputEl = @$('#search_code') + @repositoryInputEl = @$('#repository_ref') + @scopeInputEl = @$('#scope') + + @saveOriginalState() + @createAutocomplete() + @bindEvents() + + $: (selector) -> + @wrap.find(selector) + + saveOriginalState: -> + @originalState = @serializeState() + + restoreOriginalState: -> + inputs = Object.keys @originalState + + for input in inputs + @$("##{input}").val(@originalState[input]) + + + if @originalState._location is '' + @locationBadgeEl.html('') + else + @addLocationBadge( + value: @originalState._location + ) + + serializeState: -> + { + # Search Criteria + project_id: @projectInputEl.val() + group_id: @groupInputEl.val() + search_code: @searchCodeInputEl.val() + repository_ref: @repositoryInputEl.val() + + # Location badge + _location: $.trim(@locationText.text()) + } + + createAutocomplete: -> + @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef + + @catComplete = @searchInput.catcomplete + appendTo: 'form.navbar-form' + source: @autocompletePath + @query minLength: 1 - select: (event, ui) -> - location.href = ui.item.url + close: (e) -> + e.preventDefault() + + select: (event, ui) => + # Pressing enter choses an alternative + if event.keyCode is @keyCode.ENTER + @goToResult(ui.item) + else + # Pressing tab sets the scope + if event.keyCode is @keyCode.TAB and ui.item.scope? + @setLocationBadge(ui.item) + @searchInput + .val('') # remove selected value from input + .focus() + else + # If option is not a scope go to page + @goToResult(ui.item) + + # Return false to avoid focus on the next element + return false + + + bindEvents: -> + @searchInput.on 'keydown', @onSearchKeyDown + @wrap.on 'click', '.remove-badge', @onRemoveLocationBadgeClick + + onRemoveLocationBadgeClick: (e) => + e.preventDefault() + @removeLocationBadge() + @searchInput.focus() + + onSearchKeyDown: (e) => + # Remove tag when pressing backspace and input search is empty + if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is '' + @removeLocationBadge() + @destroyAutocomplete() + @searchInput.focus() + else if e.keyCode is @keyCode.ESCAPE + @restoreOriginalState() + else + # Create new autocomplete instance if it's not created + @createAutocomplete() unless @catcomplete? + + addLocationBadge: (item) -> + category = if item.category? then "#{item.category}: " else '' + value = if item.value? then item.value else '' + + html = " + #{category}#{value} + x + " + @locationBadgeEl.html(html) + + setLocationBadge: (item) -> + @addLocationBadge(item) + + # Reset input states + @resetSearchState() + + switch item.scope + when 'projects' + @projectInputEl.val(item.id) + # @searchCodeInputEl.val('true') # TODO: always true for projects? + # @repositoryInputEl.val('master') # TODO: always master? + + when 'groups' + @groupInputEl.val(item.id) + + removeLocationBadge: -> + @locationBadgeEl.empty() + + # Reset state + @resetSearchState() + + resetSearchState: -> + # Remove scope + @scopeInputEl.val('') + + # Remove group + @groupInputEl.val('') + + # Remove project id + @projectInputEl.val('') + + # Remove code search + @searchCodeInputEl.val('') + + # Remove repository ref + @repositoryInputEl.val('') + + goToResult: (result) -> + location.href = result.url + + destroyAutocomplete: -> + @catComplete.destroy() if @catcomplete? + @catComplete = null diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 494dad0b41e..9102fd6d501 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -23,45 +23,45 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { label: "Profile settings", url: profile_path }, - { label: "SSH Keys", url: profile_keys_path }, - { label: "Dashboard", url: root_path }, - { label: "Admin Section", url: admin_root_path }, + { category: "Settings", label: "Profile settings", url: profile_path }, + { category: "Settings", label: "SSH Keys", url: profile_keys_path }, + { category: "Settings", label: "Dashboard", url: root_path }, + { category: "Settings", label: "Admin Section", url: admin_root_path }, ] end # Autocomplete results for internal help pages def help_autocomplete [ - { label: "help: API Help", url: help_page_path("api", "README") }, - { label: "help: Markdown Help", url: help_page_path("markdown", "markdown") }, - { label: "help: Permissions Help", url: help_page_path("permissions", "permissions") }, - { label: "help: Public Access Help", url: help_page_path("public_access", "public_access") }, - { label: "help: Rake Tasks Help", url: help_page_path("raketasks", "README") }, - { label: "help: SSH Keys Help", url: help_page_path("ssh", "README") }, - { label: "help: System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { label: "help: Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, - { label: "help: Workflow Help", url: help_page_path("workflow", "README") }, + { category: "Help", label: "API Help", url: help_page_path("api", "README") }, + { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") }, + { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") }, + { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") }, + { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") }, + { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") }, + { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, + { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") }, ] end # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - prefix = search_result_sanitize(@project.name_with_namespace) + prefix = "Project - " + search_result_sanitize(@project.name_with_namespace) ref = @ref || @project.repository.root_ref [ - { label: "#{prefix} - Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { label: "#{prefix} - Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: prefix, label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { category: prefix, label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { category: prefix, label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { category: prefix, label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { category: prefix, label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { category: prefix, label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { category: prefix, label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { category: prefix, label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { category: prefix, label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: prefix, label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] @@ -72,7 +72,10 @@ module SearchHelper def groups_autocomplete(term, limit = 5) current_user.authorized_groups.search(term).limit(limit).map do |group| { - label: "group: #{search_result_sanitize(group.name)}", + category: "Groups", + scope: "groups", + id: group.id, + label: "#{search_result_sanitize(group.name)}", url: group_path(group) } end @@ -83,7 +86,11 @@ module SearchHelper current_user.authorized_projects.search_by_title(term). sorted_by_stars.non_archived.limit(limit).map do |p| { - label: "project: #{search_result_sanitize(p.name_with_namespace)}", + category: "Projects", + scope: "projects", + id: p.id, + value: "#{search_result_sanitize(p.name)}", + label: "#{search_result_sanitize(p.name_with_namespace)}", url: namespace_project_path(p.namespace, p) } end diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 54af2c3063c..c5002893831 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,10 +1,12 @@ .search = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| + = render 'shared/location_badge' = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1" = hidden_field_tag :group_id, @group.try(:id) - - if @project && @project.persisted? - = hidden_field_tag :project_id, @project.id + = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '' + + - if @project && @project.persisted? - if current_controller?(:issues) = hidden_field_tag :scope, 'issues' - elsif current_controller?(:merge_requests) @@ -21,10 +23,3 @@ = hidden_field_tag :repository_ref, @ref = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } - -:javascript - $('.search-input').on('keyup', function(e) { - if (e.keyCode == 27) { - $('.search-input').blur(); - } - }); diff --git a/app/views/shared/_location_badge.html.haml b/app/views/shared/_location_badge.html.haml new file mode 100644 index 00000000000..dfe8bc010d6 --- /dev/null +++ b/app/views/shared/_location_badge.html.haml @@ -0,0 +1,13 @@ +- if controller.controller_path =~ /^groups/ + - label = 'This group' +- if controller.controller_path =~ /^projects/ + - label = 'This project' + +.search-location-badge + - if label.present? + %span.label.label-primary + %i.location-text + = label + + %a.remove-badge{href: '#'} + x From 6a7f4a0767f059a2a3e4e4a4999046cffa860561 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 8 Mar 2016 19:39:14 -0500 Subject: [PATCH 036/264] Apply styling and tweaks to autocomplete dropdown --- .../lib/category_autocomplete.js.coffee | 32 +++++++++ .../javascripts/search_autocomplete.js.coffee | 35 ++++++++-- app/assets/stylesheets/framework/forms.scss | 34 --------- app/assets/stylesheets/framework/header.scss | 20 ------ app/assets/stylesheets/framework/jquery.scss | 34 ++++++++- app/assets/stylesheets/pages/search.scss | 70 +++++++++++++++++++ app/helpers/search_helper.rb | 21 +++--- app/views/layouts/_search.html.haml | 13 ++-- app/views/shared/_location_badge.html.haml | 13 ++-- 9 files changed, 186 insertions(+), 86 deletions(-) diff --git a/app/assets/javascripts/lib/category_autocomplete.js.coffee b/app/assets/javascripts/lib/category_autocomplete.js.coffee index 490032dc782..c85fabbcd5b 100644 --- a/app/assets/javascripts/lib/category_autocomplete.js.coffee +++ b/app/assets/javascripts/lib/category_autocomplete.js.coffee @@ -14,4 +14,36 @@ $.widget( "custom.catcomplete", $.ui.autocomplete, if item.category? li.attr('aria-label', item.category + " : " + item.label) + + _renderItem: (ul, item) -> + # Highlight occurrences + item.label = item.label.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + $.ui.autocomplete.escapeRegex(this.term) + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); + + return $( "
  • " ) + .data( "item.autocomplete", item ) + .append( "#{item.label}" ) + .appendTo( ul ); + + _resizeMenu: -> + if (isNaN(this.options.maxShowItems)) + return + + ul = this.menu.element.css(overflowX: '', overflowY: '', width: '', maxHeight: '') + + lis = ul.children('li').css('whiteSpace', 'nowrap'); + + if (lis.length > this.options.maxShowItems) + ulW = ul.prop('clientWidth') + + ul.css( + overflowX: 'hidden' + overflowY: 'auto' + maxHeight: lis.eq(0).outerHeight() * this.options.maxShowItems + 1 + ) + + barW = ulW - ul.prop('clientWidth'); + ul.width('+=' + barW); + + # Original code from jquery.ui.autocomplete.js _resizeMenu() + ul.outerWidth(Math.max(ul.outerWidth() + 1, this.element.outerWidth())); ) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index df31b07910c..a6d5ab65239 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -24,7 +24,10 @@ class @SearchAutocomplete @scopeInputEl = @$('#scope') @saveOriginalState() - @createAutocomplete() + + if @locationBadgeEl.is(':empty') + @createAutocomplete() + @bindEvents() $: (selector) -> @@ -66,6 +69,12 @@ class @SearchAutocomplete appendTo: 'form.navbar-form' source: @autocompletePath + @query minLength: 1 + maxShowItems: 15 + position: + # { my: "left top", at: "left bottom", collision: "none" } + my: "left-10 top+9" + at: "left bottom" + collision: "none" close: (e) -> e.preventDefault() @@ -89,7 +98,9 @@ class @SearchAutocomplete bindEvents: -> - @searchInput.on 'keydown', @onSearchKeyDown + @searchInput.on 'keydown', @onSearchInputKeyDown + @searchInput.on 'focus', @onSearchInputFocus + @searchInput.on 'blur', @onSearchInputBlur @wrap.on 'click', '.remove-badge', @onRemoveLocationBadgeClick onRemoveLocationBadgeClick: (e) => @@ -97,7 +108,7 @@ class @SearchAutocomplete @removeLocationBadge() @searchInput.focus() - onSearchKeyDown: (e) => + onSearchInputKeyDown: (e) => # Remove tag when pressing backspace and input search is empty if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is '' @removeLocationBadge() @@ -106,14 +117,24 @@ class @SearchAutocomplete else if e.keyCode is @keyCode.ESCAPE @restoreOriginalState() else - # Create new autocomplete instance if it's not created - @createAutocomplete() unless @catcomplete? + # Create new autocomplete if hasn't been created yet and there's no badge + if !@catComplete? and @locationBadgeEl.is(':empty') + @createAutocomplete() + + onSearchInputFocus: => + @wrap.addClass('search-active') + + onSearchInputBlur: => + @wrap.removeClass('search-active') + + # If input is blank then restore state + @restoreOriginalState() if @searchInput.val() is '' addLocationBadge: (item) -> category = if item.category? then "#{item.category}: " else '' value = if item.value? then item.value else '' - html = " + html = " #{category}#{value} x " @@ -160,5 +181,5 @@ class @SearchAutocomplete location.href = result.url destroyAutocomplete: -> - @catComplete.destroy() if @catcomplete? + @catComplete.destroy() if @catComplete? @catComplete = null diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 4cb4129b71b..91b6451e68a 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -6,40 +6,6 @@ input { border-radius: $border-radius-base; } -input[type='search'] { - background-color: white; - padding-left: 10px; -} - -input[type='search'].search-input { - background-repeat: no-repeat; - background-position: 10px; - background-size: 16px; - background-position-x: 30%; - padding-left: 10px; - background-color: $gray-light; - - &.search-input[value=""] { - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAFu0lEQVRIia1WTahkVxH+quqce7vf6zdvJpHoIlkYJ2SiJiIokmQjgoGgIAaEIYuYXWICgojiwkmC4taFwhjcyIDusogEIwwiSSCKPwsdwzAg0SjJ9Izzk5n3+nXfe8+pqizOvd395scfsJqi6dPnnDr11Vc/NJ1OwUTosqJLCmYCHCAC2mSHs+ojZv6AO46Y+20AhIneJsafhPhXVZSXDk7qi+aOLhtQNuBmQtcarAKjTXpn2+l3u2yPunvZSABRucjcAV/eMZuM48/Go/g1d19kc4wq+e8MZjWkbI/P5t2P3RFFbv7SQdyBlBUx8N8OTuqjMcof+N94yMPrY2DMm/ytnb32J0QrY+6AqsHM4Q64O9SKDmerKDD3Oy/tNL9vk342CC8RuU6n0ymCMHb22scu7zQngtASOjUHE1BX4UUAv4b7Ow6qiXCXuz/UdvogAAweDY943/b4cAz0ZlYHXeMsnT07RVb7wMUr8ykI4H5HVkMd5Rcb4/jNURVOL5qErAaAUUdCCIJ5kx5q2nw8m39ImEAAsjpE6PStB0YfMcd1wqqG3Xn7A3PfZyyKnNjaqD4fmE/fCNKshirIyY1xvI+Av6g5QIAIIWX7cJPssboSiBBEeKmsZne0Sb8kzAUWNYyq8NvbDo0fZ6beqxuLmqOOMr/lwOh+YXpXtbjERGja9JyZ9+HxpXKb9Gj5oywRESbj+Cj1ENG1QViTGBl1FbC1We1tbVRfHWIoQkhqH9xbpE92XUbb6VJZ1R4crjRz1JWcDMJvLdoMcyAEhjuwHo8Bfndg3mbszhOY+adVlMtD3po51OwzIQiEaams7oeJhxRw1FFOVpFRRUYIhMBAFRnjOsC8IFHHUA4TQQhgAqpAiIFfGbxkIqj54ayGbL7UoOqHCniAEKHLNr26l+D9wQJzeUwMAnfHvEnLECzZRwRV++d60ptjW9VLZeolEJG6GwCCE0CFVNB+Ay0NEqoQYG4YYFu7B8IEVRt3uRzy/osIoLV9QZimWXGHUMFdmI6M64DUF2Je88R9VZqCSP+QlcF5k+4tCzSsXaqjINuK6UyE0+s/mk6/qFq8oAIL9pqMLhkGsNrOyoOIlszust3aJv0U9+kFdwjTGwWl1YdF+KWlQSZ0Se/psj8yGVdg5tJyfH96EBWmLtoEMwMzMFt031NzGWLLzKhC+KV7H5ZeeaMOPxemma2x68puc0LN3+/u6LJiePS6MKHvn4wu6cPzJj0hsioeMfDrEvjv5r6W9gBvjKJujuKzQ0URIZj75NylvT+mbHfXQa4rwAMaVRTMm/SFyzvNy0yF6+4AM+1ubcSnqkAIUjQKl1RKSbE5jt+vovx1MBqF0WW7/d1Z80ab9BtmuJ3Xk5cJKds9TZt/uLPXvtiTrQ+dIwqfAejUvM1os6FNikXKUHfQ+ekUsXT5u85enJ0CaBSkkGEo1syUQ+DfMdE/4GA1uzupf9zdbzhOmLsF4efHVXjaHHAzmDtGdQRd/Nc5wAEJjNki3XfhyvwVNz80xANrht3LsENY9cBBdN1L9GUyyvFRFZ42t75sBvCQRykbRlU4tT2pPxoCvzx09d4GmPs200M6wKdWSDGK8mppYSWdhAlt0qeaLv+IadXU9/Evq4FAZ8ej+LmtcTxaRX4NWI0Uag5Vg1p5MYg8BnlhXIdPHDow+vTWZvVMVttXDLqkTzZdPj6Qii6cP1cSvIdl3iQkNYyi9HH0I22y+93tY3DcQkTZgQtM+POoCr8x97eylkmtrgKuztrvXJ21x/aNKuqIkZ/fntRfCdcTfhUTAIhRzoDojJD0aSNLLwMzmpT7+JaLtyf1MwDo6qz9djFaUq3t9MlFmy/c1OCSceY9fMsVaL9mvH9ocXdkdWxv1scAePG0THAhMOaLdOw/Gvxfxb1w4eCapyIENUcV5M3/u8FitAxZ25P6GAHT3UX39Srw+QOb1ZffA98Dl2Wy1BYkAAAAAElFTkSuQmCC'); - } - - &.search-input::-webkit-input-placeholder { - text-align: center; - } - - &.search-input:-moz-placeholder { /* Firefox 18- */ - text-align: center; - } - - &.search-input::-moz-placeholder { /* Firefox 19+ */ - text-align: center; - } - - &.search-input:-ms-input-placeholder { - text-align: center; - } -} - input[type='text'].danger { background: #f2dede!important; border-color: #d66; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 71a7ecab8ef..a6c9fce5b89 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -112,26 +112,6 @@ header { } } - .search { - margin-right: 10px; - margin-left: 10px; - margin-top: ($header-height - 36) / 2; - - form { - margin: 0; - padding: 0; - } - - .search-input { - width: 220px; - - &:focus { - @include box-shadow(none); - outline: none; - } - } - } - .impersonation i { color: $red-normal; } diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 525ed81b059..e0d655d3054 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -23,9 +23,39 @@ padding: 0; margin-top: 2px; z-index: 1001; + width: 240px; + margin-bottom: 0; + padding: 10px 10px; + font-size: 14px; + font-weight: normal; + background-color: $dropdown-bg; + border: 1px solid $dropdown-border-color; + border-radius: $border-radius-base; + box-shadow: 0 2px 4px $dropdown-shadow-color; - .ui-menu-item a { - padding: 4px 10px; + .ui-menu-item { + display: block; + position: relative; + padding: 0 10px; + color: $dropdown-link-color; + line-height: 34px; + text-overflow: ellipsis; + border-radius: 2px; + white-space: nowrap; + overflow: hidden; + border: none; + + &.ui-state-focus { + background-color: $dropdown-link-hover-bg; + text-decoration: none; + margin: 0; + } + } + + .ui-autocomplete-category { + text-transform: uppercase; + font-size: 11px; + color: #7f8fa4; } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index b6e45024644..57b88268c03 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -21,3 +21,73 @@ } } + +.search { + margin-right: 10px; + margin-left: 10px; + margin-top: ($header-height - 35) / 2; + + &.search-active { + form { + @extend .form-control:focus; + } + + .location-badge { + @include transition(all .15s); + background-color: $input-border-focus; + color: $white-light; + } + } + + form { + @extend .form-control; + margin: 0; + padding: 4px; + width: 350px; + line-height: 24px; + overflow: hidden; + } + + .location-text { + font-style: normal; + } + + .remove-badge { + display: none; + } + + .search-input { + border: none; + font-size: 14px; + outline: none; + padding: 0; + margin-left: 2px; + line-height: 25px; + width: 100%; + } + + .location-badge { + line-height: 25px; + padding: 0 5px; + border-radius: 2px; + font-size: 14px; + font-style: normal; + color: #AAAAAA; + display: inline-block; + background-color: #F5F5F5; + vertical-align: top; + } + + .search-input-container { + display: flex; + } + + .search-location-badge, .search-input-wrap { + // Fallback if flex is not supported + display: inline-block; + } + + .search-input-wrap { + width: 100%; + } +} diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 9102fd6d501..cbead1b8b74 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -48,20 +48,19 @@ module SearchHelper # Autocomplete results for the current project, if it's defined def project_autocomplete if @project && @project.repository.exists? && @project.repository.root_ref - prefix = "Project - " + search_result_sanitize(@project.name_with_namespace) ref = @ref || @project.repository.root_ref [ - { category: prefix, label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { category: prefix, label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { category: prefix, label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { category: prefix, label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { category: prefix, label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { category: prefix, label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { category: prefix, label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { category: prefix, label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { category: prefix, label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { category: prefix, label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index c5002893831..843c833b4fe 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,9 +1,12 @@ -.search - = form_tag search_path, method: :get, class: 'navbar-form pull-left' do |f| - = render 'shared/location_badge' - = search_field_tag "search", nil, placeholder: 'Search', class: "search-input form-control", spellcheck: false, tabindex: "1" - = hidden_field_tag :group_id, @group.try(:id) +.search.search-form + = form_tag search_path, method: :get, class: 'navbar-form' do |f| + .search-input-container + .search-location-badge + = render 'shared/location_badge' + .search-input-wrap + = search_field_tag "search", nil, placeholder: 'Search', class: "search-input", spellcheck: false, tabindex: "1", autocomplete: 'off' + = hidden_field_tag :group_id, @group.try(:id) = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '' - if @project && @project.persisted? diff --git a/app/views/shared/_location_badge.html.haml b/app/views/shared/_location_badge.html.haml index dfe8bc010d6..f1ecc060cf1 100644 --- a/app/views/shared/_location_badge.html.haml +++ b/app/views/shared/_location_badge.html.haml @@ -3,11 +3,10 @@ - if controller.controller_path =~ /^projects/ - label = 'This project' -.search-location-badge - - if label.present? - %span.label.label-primary - %i.location-text - = label +- if label.present? + %span.location-badge + %i.location-text + = label - %a.remove-badge{href: '#'} - x + %a.remove-badge{href: '#'} + x From 54797957087a41fdf84f33ca6a83be38729a9f64 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 8 Mar 2016 21:26:24 -0500 Subject: [PATCH 037/264] Tweak behaviours --- .../javascripts/search_autocomplete.js.coffee | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index a6d5ab65239..3cedf1c7b12 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -25,7 +25,8 @@ class @SearchAutocomplete @saveOriginalState() - if @locationBadgeEl.is(':empty') + # If there's no location badge + if !@locationBadgeEl.children().length @createAutocomplete() @bindEvents() @@ -65,7 +66,7 @@ class @SearchAutocomplete createAutocomplete: -> @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef - @catComplete = @searchInput.catcomplete + @searchInput.catcomplete appendTo: 'form.navbar-form' source: @autocompletePath + @query minLength: 1 @@ -96,6 +97,7 @@ class @SearchAutocomplete # Return false to avoid focus on the next element return false + @autocomplete = @searchInput.data 'customCatcomplete' bindEvents: -> @searchInput.on 'keydown', @onSearchInputKeyDown @@ -112,14 +114,19 @@ class @SearchAutocomplete # Remove tag when pressing backspace and input search is empty if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is '' @removeLocationBadge() - @destroyAutocomplete() + # @destroyAutocomplete() @searchInput.focus() else if e.keyCode is @keyCode.ESCAPE @restoreOriginalState() else # Create new autocomplete if hasn't been created yet and there's no badge - if !@catComplete? and @locationBadgeEl.is(':empty') - @createAutocomplete() + if @autocomplete is undefined + if !@locationBadgeEl.children().length + @createAutocomplete() + else + # There's a badge + if @locationBadgeEl.children().length + @destroyAutocomplete() onSearchInputFocus: => @wrap.addClass('search-active') @@ -181,5 +188,6 @@ class @SearchAutocomplete location.href = result.url destroyAutocomplete: -> - @catComplete.destroy() if @catComplete? - @catComplete = null + @autocomplete.destroy() if @autocomplete isnt undefined + @searchInput.attr('autocomplete', 'off') + @autocomplete = undefined From 82e3c1f257e5b2ecaf4e52a01e3d6379e98684a7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 12:45:43 -0500 Subject: [PATCH 038/264] Change hidden input id to avoid duplicated IDs The TODOs dashboard already had a #project_id input and it was causing a spec to fail --- app/assets/javascripts/search_autocomplete.js.coffee | 2 +- app/views/layouts/_search.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 3cedf1c7b12..0c4876358bd 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -17,7 +17,7 @@ class @SearchAutocomplete @locationBadgeEl = @$('.search-location-badge') @locationText = @$('.location-text') @searchInput = @$('.search-input') - @projectInputEl = @$('#project_id') + @projectInputEl = @$('#search_project_id') @groupInputEl = @$('#group_id') @searchCodeInputEl = @$('#search_code') @repositoryInputEl = @$('#repository_ref') diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 843c833b4fe..58a3cdf955e 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -7,7 +7,7 @@ = search_field_tag "search", nil, placeholder: 'Search', class: "search-input", spellcheck: false, tabindex: "1", autocomplete: 'off' = hidden_field_tag :group_id, @group.try(:id) - = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '' + = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' - if @project && @project.persisted? - if current_controller?(:issues) From ec0dfff2048b79087204de9083f4f1eca8446650 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 15:52:15 -0500 Subject: [PATCH 039/264] Add icons --- app/assets/images/spinner.svg | 1 + app/assets/stylesheets/pages/search.scss | 39 +++++++++++++++++++++++- app/views/layouts/_search.html.haml | 1 + 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/spinner.svg diff --git a/app/assets/images/spinner.svg b/app/assets/images/spinner.svg new file mode 100644 index 00000000000..3dd110cfa0f --- /dev/null +++ b/app/assets/images/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 57b88268c03..bcbdbb07ed3 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -37,6 +37,12 @@ background-color: $input-border-focus; color: $white-light; } + + .search-input-wrap { + i { + color: $input-border-focus; + } + } } form { @@ -61,7 +67,7 @@ font-size: 14px; outline: none; padding: 0; - margin-left: 2px; + margin-left: 5px; line-height: 25px; width: 100%; } @@ -89,5 +95,36 @@ .search-input-wrap { width: 100%; + position: relative; + + .search-icon { + @extend .fa-search; + @include transition(color .15s); + position: absolute; + right: 5px; + color: #E7E9ED; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + + &:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + } + } + + .ui-autocomplete-loading + .search-icon { + height: 25px; + width: 25px; + position: absolute; + right: 0; + background-image: image-url('spinner.svg'); + fill: red; + + &:before { + display: none; + } + } } } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 58a3cdf955e..a004908fb6f 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -5,6 +5,7 @@ = render 'shared/location_badge' .search-input-wrap = search_field_tag "search", nil, placeholder: 'Search', class: "search-input", spellcheck: false, tabindex: "1", autocomplete: 'off' + %i.search-icon = hidden_field_tag :group_id, @group.try(:id) = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' From 2541f59227013055733f5cdf1f36835436f196dc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 16:34:21 -0500 Subject: [PATCH 040/264] Better wording --- app/assets/javascripts/search_autocomplete.js.coffee | 8 ++++---- app/helpers/search_helper.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 0c4876358bd..b8671900862 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -84,14 +84,14 @@ class @SearchAutocomplete if event.keyCode is @keyCode.ENTER @goToResult(ui.item) else - # Pressing tab sets the scope - if event.keyCode is @keyCode.TAB and ui.item.scope? + # Pressing tab sets the location + if event.keyCode is @keyCode.TAB and ui.item.location? @setLocationBadge(ui.item) @searchInput .val('') # remove selected value from input .focus() else - # If option is not a scope go to page + # If option is not a location go to page @goToResult(ui.item) # Return false to avoid focus on the next element @@ -153,7 +153,7 @@ class @SearchAutocomplete # Reset input states @resetSearchState() - switch item.scope + switch item.location when 'projects' @projectInputEl.val(item.id) # @searchCodeInputEl.val('true') # TODO: always true for projects? diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index cbead1b8b74..de164547396 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -72,7 +72,7 @@ module SearchHelper current_user.authorized_groups.search(term).limit(limit).map do |group| { category: "Groups", - scope: "groups", + location: "groups", id: group.id, label: "#{search_result_sanitize(group.name)}", url: group_path(group) @@ -86,7 +86,7 @@ module SearchHelper sorted_by_stars.non_archived.limit(limit).map do |p| { category: "Projects", - scope: "projects", + location: "projects", id: p.id, value: "#{search_result_sanitize(p.name)}", label: "#{search_result_sanitize(p.name_with_namespace)}", From dccda7d09f69ffef50de8ea6194ad105194a41ed Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 9 Mar 2016 22:16:30 -0500 Subject: [PATCH 041/264] Replace spinner icon for th FontAwesome one --- app/assets/images/spinner.svg | 1 - app/assets/stylesheets/pages/search.scss | 12 ++---------- 2 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 app/assets/images/spinner.svg diff --git a/app/assets/images/spinner.svg b/app/assets/images/spinner.svg deleted file mode 100644 index 3dd110cfa0f..00000000000 --- a/app/assets/images/spinner.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index bcbdbb07ed3..fa45d750f29 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -115,16 +115,8 @@ } .ui-autocomplete-loading + .search-icon { - height: 25px; - width: 25px; - position: absolute; - right: 0; - background-image: image-url('spinner.svg'); - fill: red; - - &:before { - display: none; - } + @extend .fa-spinner; + @extend .fa-spin; } } } From a0c1aa6d046b8dc20b75f3a6fe5f82e5055666a2 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 13:18:38 -0500 Subject: [PATCH 042/264] Allow to pass non-asynchronous data to GitLabDropdown --- app/assets/javascripts/gl_dropdown.js.coffee | 27 +++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index c81e8bf760a..36b41072ebd 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -83,15 +83,19 @@ class GitLabDropdown search_fields = if @options.search then @options.search.fields else []; if @options.data - # Remote data - @remote = new GitLabDropdownRemote @options.data, { - dataType: @options.dataType, - beforeSend: @toggleLoading.bind(@) - success: (data) => - @fullData = data + # If data is an array + if _.isArray @options.data + @parseData @options.data + else + # Remote data + @remote = new GitLabDropdownRemote @options.data, { + dataType: @options.dataType, + beforeSend: @toggleLoading.bind(@) + success: (data) => + @fullData = data - @parseData @fullData - } + @parseData @fullData + } # Init filiterable if @options.filterable @@ -204,7 +208,12 @@ class GitLabDropdown else selected = if @options.isSelected then @options.isSelected(data) else false url = if @options.url then @options.url(data) else "#" - text = if @options.text then @options.text(data) else "" + + if @options.text? + text = @options.text(data) + else + text = data.text if data.text? + cssClass = ""; if selected From 0b893bec544e2b1816979343f5f1c669482bfca4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 13:39:28 -0500 Subject: [PATCH 043/264] Allow data with desired format --- app/assets/javascripts/gl_dropdown.js.coffee | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 36b41072ebd..cdde52a38a7 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -209,10 +209,17 @@ class GitLabDropdown selected = if @options.isSelected then @options.isSelected(data) else false url = if @options.url then @options.url(data) else "#" + # Set URL + if @options.url? + url = @options.url(data) + else + url = if data.url? then data.url else '' + + # Set Text if @options.text? text = @options.text(data) else - text = data.text if data.text? + text = if data.text? then data.text else '' cssClass = ""; From 4e486c6116002e35ce79284e9ce26f8612674021 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 14:59:50 -0500 Subject: [PATCH 044/264] Allow to pass input filter param This allow us to set a different input to filter results --- app/assets/javascripts/gl_dropdown.js.coffee | 28 +++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index cdde52a38a7..4e733165b40 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -2,7 +2,9 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] constructor: (@dropdown, @options) -> - @input = @dropdown.find(".dropdown-input .dropdown-input-field") + { + @input + } = @options # Key events timeout = "" @@ -77,14 +79,30 @@ class GitLabDropdown PAGE_TWO_CLASS = "is-page-two" ACTIVE_CLASS = "is-active" + FILTER_INPUT = '.dropdown-input .dropdown-input-field' + constructor: (@el, @options) -> - self = @ @dropdown = $(@el).parent() + + # Set Defaults + { + # If no input is passed create a default one + @filterInput = @$(FILTER_INPUT) + } = @options + + self = @ + + # If selector was passed + if _.isString(@filterInput) + @filterInput = @$(@filterInput) + + search_fields = if @options.search then @options.search.fields else []; if @options.data # If data is an array if _.isArray @options.data + @fullData = @options.data @parseData @options.data else # Remote data @@ -100,6 +118,7 @@ class GitLabDropdown # Init filiterable if @options.filterable @filter = new GitLabDropdownFilter @dropdown, + input: @filterInput remote: @options.filterRemote query: @options.data keys: @options.search.fields @@ -133,6 +152,9 @@ class GitLabDropdown if self.options.clicked self.options.clicked() + $: (selector) -> + $(selector, @dropdown) + toggleLoading: -> $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS @@ -167,7 +189,7 @@ class GitLabDropdown @remote.execute() if @options.filterable - @dropdown.find(".dropdown-input-field").focus() + @filterInput.focus() hidden: => if @options.filterable From f858ee43eacc4cfdc24e38edafe39a6d7bc5e415 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 18:00:17 -0500 Subject: [PATCH 045/264] Allow to pass header items --- app/assets/javascripts/gl_dropdown.js.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 4e733165b40..78e19064e5b 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -222,8 +222,12 @@ class GitLabDropdown renderItem: (data) -> html = "" + # Separator return "
  • " if data is "divider" + # Header + return "" if data.header? + if @options.renderRow # Call the render function html = @options.renderRow(data) From b5bd497a1f85727a76f1a604ffba30783a8457ac Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 20:38:19 -0500 Subject: [PATCH 046/264] Allow to hightlight matches --- app/assets/javascripts/gl_dropdown.js.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 78e19064e5b..e1fae7bbc50 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -252,6 +252,8 @@ class GitLabDropdown if selected cssClass = "is-active" + text = @highlightTextMatches(text, @filterInput.val()) + html = "
  • " html += "" html += text @@ -260,6 +262,15 @@ class GitLabDropdown return html + highlightTextMatches: (text, term) -> + occurrences = fuzzaldrinPlus.match(text, term) + textArr = text.split('') + textArr.forEach (character, i, textArr) -> + if i in occurrences + textArr[i] = "#{character}" + + textArr.join '' + noResults: -> html = "
  • " html += "" From 2d0f8f928ce4dd0ba2db58e2434b81f153007aec Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 11 Mar 2016 20:47:01 -0500 Subject: [PATCH 047/264] Disable highlighting by default --- app/assets/javascripts/gl_dropdown.js.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index e1fae7bbc50..53bbbfbaf5d 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -88,6 +88,7 @@ class GitLabDropdown { # If no input is passed create a default one @filterInput = @$(FILTER_INPUT) + @highlight = false } = @options self = @ @@ -252,7 +253,8 @@ class GitLabDropdown if selected cssClass = "is-active" - text = @highlightTextMatches(text, @filterInput.val()) + if @highlight + text = @highlightTextMatches(text, @filterInput.val()) html = "
  • " html += "" From 53df7263b7e898ab118714f5da818476c0a75e6d Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 14 Mar 2016 16:14:29 -0500 Subject: [PATCH 048/264] Use new dropdown class for search suggestions --- app/assets/javascripts/gl_dropdown.js.coffee | 5 +- .../javascripts/search_autocomplete.js.coffee | 266 +++++++++--------- app/assets/stylesheets/framework/jquery.scss | 6 - app/assets/stylesheets/pages/search.scss | 13 +- app/views/layouts/_search.html.haml | 6 +- 5 files changed, 154 insertions(+), 142 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 53bbbfbaf5d..3d2e3f3dbb4 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -4,6 +4,7 @@ class GitLabDropdownFilter constructor: (@dropdown, @options) -> { @input + @filterInputBlur = true } = @options # Key events @@ -19,7 +20,7 @@ class GitLabDropdownFilter blur_field = @shouldBlur e.keyCode search_text = @input.val() - if blur_field + if blur_field && @filterInputBlur @input.blur() if @options.remote @@ -89,6 +90,7 @@ class GitLabDropdown # If no input is passed create a default one @filterInput = @$(FILTER_INPUT) @highlight = false + @filterInputBlur = true } = @options self = @ @@ -119,6 +121,7 @@ class GitLabDropdown # Init filiterable if @options.filterable @filter = new GitLabDropdownFilter @dropdown, + filterInputBlur: @filterInputBlur input: @filterInput remote: @options.filterRemote query: @options.data diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index b8671900862..e21a140b2a6 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -1,21 +1,28 @@ class @SearchAutocomplete + + KEYCODE = + ESCAPE: 27 + BACKSPACE: 8 + TAB: 9 + ENTER: 13 + constructor: (opts = {}) -> { @wrap = $('.search') + @optsEl = @wrap.find('.search-autocomplete-opts') @autocompletePath = @optsEl.data('autocomplete-path') @projectId = @optsEl.data('autocomplete-project-id') || '' @projectRef = @optsEl.data('autocomplete-project-ref') || '' + } = opts - @keyCode = - ESCAPE: 27 - BACKSPACE: 8 - TAB: 9 - ENTER: 13 + # Dropdown Element + @dropdown = @wrap.find('.dropdown') @locationBadgeEl = @$('.search-location-badge') @locationText = @$('.location-text') + @scopeInputEl = @$('#scope') @searchInput = @$('.search-input') @projectInputEl = @$('#search_project_id') @groupInputEl = @$('#group_id') @@ -25,9 +32,7 @@ class @SearchAutocomplete @saveOriginalState() - # If there's no location badge - if !@locationBadgeEl.children().length - @createAutocomplete() + @searchInput.addClass('disabled') @bindEvents() @@ -37,6 +42,118 @@ class @SearchAutocomplete saveOriginalState: -> @originalState = @serializeState() + serializeState: -> + { + # Search Criteria + project_id: @projectInputEl.val() + group_id: @groupInputEl.val() + search_code: @searchCodeInputEl.val() + repository_ref: @repositoryInputEl.val() + + # Location badge + _location: $.trim(@locationText.text()) + } + + bindEvents: -> + @searchInput.on 'keydown', @onSearchInputKeyDown + @searchInput.on 'focus', @onSearchInputFocus + @searchInput.on 'blur', @onSearchInputBlur + + enableAutocomplete: -> + self = @ + @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef + dropdownMenu = self.dropdown.find('.dropdown-menu') + + @searchInput.glDropdown( + filterInputBlur: false + filterable: true + filterRemote: true + highlight: true + filterInput: 'input#search' + search: + fields: ['text'] + data: (term, callback) -> + $.ajax + url: self.autocompletePath + self.query + data: + term: term + beforeSend: -> + # dropdownMenu.addClass 'is-loading' + success: (response) -> + data = [] + + # Save groups ordering according to server response + groupNames = _.unique(_.pluck(response, 'category')) + + # Group results by category name + groups = _.groupBy response, (item) -> + item.category + + # List results + for groupName in groupNames + + # Add group header before list each group + data.push + header: groupName + + # List group + for item in groups[groupName] + data.push + text: item.label + url: item.url + + callback(data) + complete: -> + # dropdownMenu.removeClass 'is-loading' + + ) + + @dropdown.addClass('open') + @searchInput.removeClass('disabled') + @autocomplete = true; + + onDropdownOpen: (e) => + @dropdown.dropdown('toggle') + + onSearchInputKeyDown: (e) => + # Remove tag when pressing backspace and input search is empty + if e.keyCode is KEYCODE.BACKSPACE and e.currentTarget.value is '' + @removeLocationBadge() + @searchInput.focus() + + else if e.keyCode is KEYCODE.ESCAPE + @searchInput.val('') + @restoreOriginalState() + else + # Create new autocomplete if it hasn't been created yet and there's no badge + if @autocomplete is undefined + if !@badgePresent() + @enableAutocomplete() + else + # There's a badge + if @badgePresent() + @disableAutocomplete() + + onSearchInputFocus: => + @wrap.addClass('search-active') + + onSearchInputBlur: => + @wrap.removeClass('search-active') + + # If input is blank then restore state + if @searchInput.val() is '' + @restoreOriginalState() + + addLocationBadge: (item) -> + category = if item.category? then "#{item.category}: " else '' + value = if item.value? then item.value else '' + + html = " + #{category}#{value} + x + " + @locationBadgeEl.html(html) + restoreOriginalState: -> inputs = Object.keys @originalState @@ -51,122 +168,14 @@ class @SearchAutocomplete value: @originalState._location ) - serializeState: -> - { - # Search Criteria - project_id: @projectInputEl.val() - group_id: @groupInputEl.val() - search_code: @searchCodeInputEl.val() - repository_ref: @repositoryInputEl.val() + @dropdown.removeClass 'open' - # Location badge - _location: $.trim(@locationText.text()) - } + # Only add class if there's a badge + if @badgePresent() + @searchInput.addClass 'disabled' - createAutocomplete: -> - @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef - - @searchInput.catcomplete - appendTo: 'form.navbar-form' - source: @autocompletePath + @query - minLength: 1 - maxShowItems: 15 - position: - # { my: "left top", at: "left bottom", collision: "none" } - my: "left-10 top+9" - at: "left bottom" - collision: "none" - close: (e) -> - e.preventDefault() - - select: (event, ui) => - # Pressing enter choses an alternative - if event.keyCode is @keyCode.ENTER - @goToResult(ui.item) - else - # Pressing tab sets the location - if event.keyCode is @keyCode.TAB and ui.item.location? - @setLocationBadge(ui.item) - @searchInput - .val('') # remove selected value from input - .focus() - else - # If option is not a location go to page - @goToResult(ui.item) - - # Return false to avoid focus on the next element - return false - - @autocomplete = @searchInput.data 'customCatcomplete' - - bindEvents: -> - @searchInput.on 'keydown', @onSearchInputKeyDown - @searchInput.on 'focus', @onSearchInputFocus - @searchInput.on 'blur', @onSearchInputBlur - @wrap.on 'click', '.remove-badge', @onRemoveLocationBadgeClick - - onRemoveLocationBadgeClick: (e) => - e.preventDefault() - @removeLocationBadge() - @searchInput.focus() - - onSearchInputKeyDown: (e) => - # Remove tag when pressing backspace and input search is empty - if e.keyCode is @keyCode.BACKSPACE and e.currentTarget.value is '' - @removeLocationBadge() - # @destroyAutocomplete() - @searchInput.focus() - else if e.keyCode is @keyCode.ESCAPE - @restoreOriginalState() - else - # Create new autocomplete if hasn't been created yet and there's no badge - if @autocomplete is undefined - if !@locationBadgeEl.children().length - @createAutocomplete() - else - # There's a badge - if @locationBadgeEl.children().length - @destroyAutocomplete() - - onSearchInputFocus: => - @wrap.addClass('search-active') - - onSearchInputBlur: => - @wrap.removeClass('search-active') - - # If input is blank then restore state - @restoreOriginalState() if @searchInput.val() is '' - - addLocationBadge: (item) -> - category = if item.category? then "#{item.category}: " else '' - value = if item.value? then item.value else '' - - html = " - #{category}#{value} - x - " - @locationBadgeEl.html(html) - - setLocationBadge: (item) -> - @addLocationBadge(item) - - # Reset input states - @resetSearchState() - - switch item.location - when 'projects' - @projectInputEl.val(item.id) - # @searchCodeInputEl.val('true') # TODO: always true for projects? - # @repositoryInputEl.val('master') # TODO: always master? - - when 'groups' - @groupInputEl.val(item.id) - - removeLocationBadge: -> - @locationBadgeEl.empty() - - # Reset state - @resetSearchState() + badgePresent: -> + @locationBadgeEl.children().length resetSearchState: -> # Remove scope @@ -184,10 +193,13 @@ class @SearchAutocomplete # Remove repository ref @repositoryInputEl.val('') - goToResult: (result) -> - location.href = result.url + removeLocationBadge: -> + @locationBadgeEl.empty() - destroyAutocomplete: -> - @autocomplete.destroy() if @autocomplete isnt undefined - @searchInput.attr('autocomplete', 'off') + # Reset state + @resetSearchState() + + disableAutocomplete: -> + if @autocomplete isnt undefined + @searchInput.addClass('disabled') @autocomplete = undefined diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index e0d655d3054..7af307940da 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -51,12 +51,6 @@ margin: 0; } } - - .ui-autocomplete-category { - text-transform: uppercase; - font-size: 11px; - color: #7f8fa4; - } } .ui-state-default { diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index fa45d750f29..4a02f75719b 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -21,7 +21,6 @@ } } - .search { margin-right: 10px; margin-left: 10px; @@ -51,7 +50,6 @@ padding: 4px; width: 350px; line-height: 24px; - overflow: hidden; } .location-text { @@ -69,7 +67,7 @@ padding: 0; margin-left: 5px; line-height: 25px; - width: 100%; + width: 98%; } .location-badge { @@ -89,7 +87,7 @@ } .search-location-badge, .search-input-wrap { - // Fallback if flex is not supported + // Fallback if flexbox is not supported display: inline-block; } @@ -103,6 +101,7 @@ position: absolute; right: 5px; color: #E7E9ED; + top: 0; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -114,9 +113,9 @@ } } - .ui-autocomplete-loading + .search-icon { - @extend .fa-spinner; - @extend .fa-spin; + .dropdown-header { + text-transform: uppercase; + font-size: 11px; } } } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index a004908fb6f..f051e7a1867 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -4,7 +4,11 @@ .search-location-badge = render 'shared/location_badge' .search-input-wrap - = search_field_tag "search", nil, placeholder: 'Search', class: "search-input", spellcheck: false, tabindex: "1", autocomplete: 'off' + .dropdown{ data: {url: search_autocomplete_path } } + = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } + .dropdown-menu.dropdown-select + = dropdown_content + = dropdown_loading %i.search-icon = hidden_field_tag :group_id, @group.try(:id) From 73e043d196da3e9865677ec269b25a0a5178ed9b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 14 Mar 2016 16:23:41 -0500 Subject: [PATCH 049/264] Delete unused file --- .../lib/category_autocomplete.js.coffee | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 app/assets/javascripts/lib/category_autocomplete.js.coffee diff --git a/app/assets/javascripts/lib/category_autocomplete.js.coffee b/app/assets/javascripts/lib/category_autocomplete.js.coffee deleted file mode 100644 index c85fabbcd5b..00000000000 --- a/app/assets/javascripts/lib/category_autocomplete.js.coffee +++ /dev/null @@ -1,49 +0,0 @@ -$.widget( "custom.catcomplete", $.ui.autocomplete, - _create: -> - @_super(); - @widget().menu("option", "items", "> :not(.ui-autocomplete-category)") - - _renderMenu: (ul, items) -> - currentCategory = '' - $.each items, (index, item) => - if item.category isnt currentCategory - ul.append("
  • #{item.category}
  • ") - currentCategory = item.category - - li = @_renderItemData(ul, item) - - if item.category? - li.attr('aria-label', item.category + " : " + item.label) - - _renderItem: (ul, item) -> - # Highlight occurrences - item.label = item.label.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + $.ui.autocomplete.escapeRegex(this.term) + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); - - return $( "
  • " ) - .data( "item.autocomplete", item ) - .append( "#{item.label}" ) - .appendTo( ul ); - - _resizeMenu: -> - if (isNaN(this.options.maxShowItems)) - return - - ul = this.menu.element.css(overflowX: '', overflowY: '', width: '', maxHeight: '') - - lis = ul.children('li').css('whiteSpace', 'nowrap'); - - if (lis.length > this.options.maxShowItems) - ulW = ul.prop('clientWidth') - - ul.css( - overflowX: 'hidden' - overflowY: 'auto' - maxHeight: lis.eq(0).outerHeight() * this.options.maxShowItems + 1 - ) - - barW = ulW - ul.prop('clientWidth'); - ul.width('+=' + barW); - - # Original code from jquery.ui.autocomplete.js _resizeMenu() - ul.outerWidth(Math.max(ul.outerWidth() + 1, this.element.outerWidth())); - ) From cdd7e1855e8fd7e35583454166fc21dbb9f31b10 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 14 Mar 2016 22:04:22 -0500 Subject: [PATCH 050/264] Fixes failing spec --- app/assets/javascripts/gl_dropdown.js.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 3d2e3f3dbb4..ca7b22bd816 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -237,13 +237,12 @@ class GitLabDropdown html = @options.renderRow(data) else selected = if @options.isSelected then @options.isSelected(data) else false - url = if @options.url then @options.url(data) else "#" # Set URL if @options.url? url = @options.url(data) else - url = if data.url? then data.url else '' + url = if data.url? then data.url else '#' # Set Text if @options.text? From 46f9790ceb30cf00a3f9d11b8ef0121294fc1a40 Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 18 Mar 2016 14:06:08 -0400 Subject: [PATCH 051/264] Fixing rebase conflicts --- app/assets/stylesheets/framework/jquery.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 7af307940da..eb3fbd9155b 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -19,8 +19,6 @@ } &.ui-autocomplete { - border-color: #ddd; - padding: 0; margin-top: 2px; z-index: 1001; width: 240px; From 3898bffd62f72b9b8ffe3c6f5739fe71ff19f433 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 18 Mar 2016 17:35:26 -0500 Subject: [PATCH 052/264] Code improvements --- app/assets/javascripts/gl_dropdown.js.coffee | 21 ++++---- .../javascripts/search_autocomplete.js.coffee | 51 ++++++++----------- 2 files changed, 31 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index ca7b22bd816..d9a4cb1771b 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -20,7 +20,7 @@ class GitLabDropdownFilter blur_field = @shouldBlur e.keyCode search_text = @input.val() - if blur_field && @filterInputBlur + if blur_field and @filterInputBlur @input.blur() if @options.remote @@ -88,7 +88,7 @@ class GitLabDropdown # Set Defaults { # If no input is passed create a default one - @filterInput = @$(FILTER_INPUT) + @filterInput = @getElement(FILTER_INPUT) @highlight = false @filterInputBlur = true } = @options @@ -97,8 +97,7 @@ class GitLabDropdown # If selector was passed if _.isString(@filterInput) - @filterInput = @$(@filterInput) - + @filterInput = @getElement(@filterInput) search_fields = if @options.search then @options.search.fields else []; @@ -156,8 +155,9 @@ class GitLabDropdown if self.options.clicked self.options.clicked() - $: (selector) -> - $(selector, @dropdown) + # Finds an element inside wrapper element + getElement: (selector) -> + @dropdown.find selector toggleLoading: -> $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS @@ -268,12 +268,9 @@ class GitLabDropdown highlightTextMatches: (text, term) -> occurrences = fuzzaldrinPlus.match(text, term) - textArr = text.split('') - textArr.forEach (character, i, textArr) -> - if i in occurrences - textArr[i] = "#{character}" - - textArr.join '' + text.split('').map((character, i) -> + if i in occurrences then "#{character}" else character + ).join('') noResults: -> html = "
  • " diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index e21a140b2a6..18fa3b86d16 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -20,15 +20,15 @@ class @SearchAutocomplete # Dropdown Element @dropdown = @wrap.find('.dropdown') - @locationBadgeEl = @$('.search-location-badge') - @locationText = @$('.location-text') - @scopeInputEl = @$('#scope') - @searchInput = @$('.search-input') - @projectInputEl = @$('#search_project_id') - @groupInputEl = @$('#group_id') - @searchCodeInputEl = @$('#search_code') - @repositoryInputEl = @$('#repository_ref') - @scopeInputEl = @$('#scope') + @locationBadgeEl = @getElement('.search-location-badge') + @locationText = @getElement('.location-text') + @scopeInputEl = @getElement('#scope') + @searchInput = @getElement('.search-input') + @projectInputEl = @getElement('#search_project_id') + @groupInputEl = @getElement('#group_id') + @searchCodeInputEl = @getElement('#search_code') + @repositoryInputEl = @getElement('#repository_ref') + @scopeInputEl = @getElement('#scope') @saveOriginalState() @@ -36,7 +36,8 @@ class @SearchAutocomplete @bindEvents() - $: (selector) -> + # Finds an element inside wrapper element + getElement: (selector) -> @wrap.find(selector) saveOriginalState: -> @@ -60,11 +61,9 @@ class @SearchAutocomplete @searchInput.on 'blur', @onSearchInputBlur enableAutocomplete: -> - self = @ - @query = "?project_id=" + @projectId + "&project_ref=" + @projectRef - dropdownMenu = self.dropdown.find('.dropdown-menu') - - @searchInput.glDropdown( + dropdownMenu = @dropdown.find('.dropdown-menu') + _this = @ + @searchInput.glDropdown filterInputBlur: false filterable: true filterRemote: true @@ -73,13 +72,11 @@ class @SearchAutocomplete search: fields: ['text'] data: (term, callback) -> - $.ajax - url: self.autocompletePath + self.query - data: + $.get(_this.autocompletePath, { + project_id: _this.projectId + project_ref: _this.projectRef term: term - beforeSend: -> - # dropdownMenu.addClass 'is-loading' - success: (response) -> + }, (response) -> data = [] # Save groups ordering according to server response @@ -101,16 +98,12 @@ class @SearchAutocomplete data.push text: item.label url: item.url - callback(data) - complete: -> - # dropdownMenu.removeClass 'is-loading' - ) @dropdown.addClass('open') @searchInput.removeClass('disabled') - @autocomplete = true; + @autocomplete = true onDropdownOpen: (e) => @dropdown.dropdown('toggle') @@ -158,7 +151,7 @@ class @SearchAutocomplete inputs = Object.keys @originalState for input in inputs - @$("##{input}").val(@originalState[input]) + @getElement("##{input}").val(@originalState[input]) if @originalState._location is '' @@ -200,6 +193,6 @@ class @SearchAutocomplete @resetSearchState() disableAutocomplete: -> - if @autocomplete isnt undefined + if @autocomplete? @searchInput.addClass('disabled') - @autocomplete = undefined + @autocomplete = null From 9008457a8de36ce7d2b673e082df3783c9c71892 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 18 Mar 2016 21:43:26 -0500 Subject: [PATCH 053/264] Save instance and avoid multiple instantiation --- app/assets/javascripts/gl_dropdown.js.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index d9a4cb1771b..f74da006e64 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -117,7 +117,7 @@ class GitLabDropdown @parseData @fullData } - # Init filiterable + # Init filterable if @options.filterable @filter = new GitLabDropdownFilter @dropdown, filterInputBlur: @filterInputBlur @@ -327,4 +327,6 @@ class GitLabDropdown $.fn.glDropdown = (opts) -> return @.each -> - new GitLabDropdown @, opts + if (!$.data @, 'glDropdown') + $.data(@, 'glDropdown', new GitLabDropdown @, opts) + From b4593a1b63a93eaaa77a0a47a7689e32f53b1a18 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 18 Mar 2016 21:50:49 -0500 Subject: [PATCH 054/264] Fix multiple ajax calls and plugin instantiation --- .../javascripts/search_autocomplete.js.coffee | 55 +++++++++++++------ 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 18fa3b86d16..25db343ca46 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -33,6 +33,7 @@ class @SearchAutocomplete @saveOriginalState() @searchInput.addClass('disabled') + @autocomplete = false @bindEvents() @@ -61,8 +62,12 @@ class @SearchAutocomplete @searchInput.on 'blur', @onSearchInputBlur enableAutocomplete: -> + return if @autocomplete + dropdownMenu = @dropdown.find('.dropdown-menu') _this = @ + loading = false + @searchInput.glDropdown filterInputBlur: false filterable: true @@ -72,7 +77,19 @@ class @SearchAutocomplete search: fields: ['text'] data: (term, callback) -> - $.get(_this.autocompletePath, { + # Ensure this is not called when autocomplete is disabled because + # this method still will be called because `GitLabDropdownFilter` is triggering this on keyup + return if _this.autocomplete is false + + # Do not trigger request if input is empty + return if _this.searchInput.val() is '' + + # Prevent multiple ajax calls + return if loading + + loading = true + + jqXHR = $.get(_this.autocompletePath, { project_id: _this.projectId project_ref: _this.projectRef term: term @@ -99,7 +116,8 @@ class @SearchAutocomplete text: item.label url: item.url callback(data) - ) + ).always -> + loading = false @dropdown.addClass('open') @searchInput.removeClass('disabled') @@ -109,23 +127,26 @@ class @SearchAutocomplete @dropdown.dropdown('toggle') onSearchInputKeyDown: (e) => - # Remove tag when pressing backspace and input search is empty - if e.keyCode is KEYCODE.BACKSPACE and e.currentTarget.value is '' - @removeLocationBadge() - @searchInput.focus() + switch e.keyCode + when KEYCODE.BACKSPACE + if e.currentTarget.value is '' + @removeLocationBadge() + @searchInput.focus() + when KEYCODE.ESCAPE + if @badgePresent() + else + @restoreOriginalState() - else if e.keyCode is KEYCODE.ESCAPE - @searchInput.val('') - @restoreOriginalState() - else - # Create new autocomplete if it hasn't been created yet and there's no badge - if @autocomplete is undefined - if !@badgePresent() - @enableAutocomplete() + # If after restoring there's a badge + @disableAutocomplete() if @badgePresent() else - # There's a badge if @badgePresent() @disableAutocomplete() + else + @enableAutocomplete() + + # Avoid falsy value to be returned + return onSearchInputFocus: => @wrap.addClass('search-active') @@ -193,6 +214,6 @@ class @SearchAutocomplete @resetSearchState() disableAutocomplete: -> - if @autocomplete? + if @autocomplete @searchInput.addClass('disabled') - @autocomplete = null + @autocomplete = false From c767f35c7f7e4aa9cabbe27db06c1a4a1eb46f54 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 21 Mar 2016 09:52:38 +0000 Subject: [PATCH 055/264] Updated based on feedback --- .../merge_request_widget.js.coffee | 52 ++++++++--------- .../merge_requests/widget/_heading.html.haml | 56 ++++++++++++------- 2 files changed, 61 insertions(+), 47 deletions(-) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 877e85a12e9..43671ee3939 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -8,10 +8,11 @@ class @MergeRequestWidget constructor: (@opts) -> @firstCICheck = true - @getCIStatus() - notifyPermissions() @readyForCICheck = true - # clear the build poller + clearInterval @fetchBuildStatusInterval + + @pollCIStatus() + notifyPermissions() mergeInProgress: (deleteSourceBranch = false)-> $.ajax @@ -39,23 +40,32 @@ class @MergeRequestWidget else status - getCIStatus: -> - _this = @ + pollCIStatus: -> @fetchBuildStatusInterval = setInterval ( => return if not @readyForCICheck - $.getJSON @opts.ci_status_url, (data) => - @readyForCICheck = true + @getCIStatus(true) - if @firstCICheck - @firstCICheck = false - @opts.ci_status = data.status + @readyForCICheck = false + ), 5000 - if data.status isnt @opts.ci_status - @showCIState data.status - if data.coverage - @showCICoverage data.coverage + getCIStatus: (showNotification) -> + _this = @ + $('.ci-widget-fetching').show() + $.getJSON @opts.ci_status_url, (data) => + @readyForCICheck = true + + if @firstCICheck + @firstCICheck = false + @opts.ci_status = data.status + + if data.status isnt @opts.ci_status + @showCIStatus data.status + if data.coverage + @showCICoverage data.coverage + + if showNotification message = @opts.ci_message.replace('{{status}}', @ciLabelForStatus(data.status)) message = message.replace('{{sha}}', data.sha) message = message.replace('{{title}}', data.title) @@ -69,19 +79,9 @@ class @MergeRequestWidget Turbolinks.visit _this.opts.builds_path ) - @opts.ci_status = data.status + @opts.ci_status = data.status - @readyForCICheck = false - ), 5000 - - getCIState: -> - $('.ci-widget-fetching').show() - $.getJSON @opts.ci_status_url, (data) => - @showCIState data.status - if data.coverage - @showCICoverage data.coverage - - showCIState: (state) -> + showCIStatus: (state) -> $('.ci_widget').hide() allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"] if state in allowed_states diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 2ee8e2de0e8..2ec0d20a879 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,12 +1,24 @@ -- if @ci_commit or @merge_request.has_ci? +- if @ci_commit .mr-widget-heading - - if @merge_request.has_ci? - .ci_widget.ci-widget-fetching - = icon('spinner spin') - %span - Checking CI status for #{@merge_request.last_commit_short_sha}… - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status}", style: ("display:none" unless status == @ci_commit.status) } + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @ci_commit.status == status) } + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.last_commit + = succeed "." do + = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" + %span.ci-coverage + = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + +- elsif @merge_request.has_ci? + - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX + - # Remove in later versions when services like Jenkins will set CI status via Commit status API + .mr-widget-heading + - %w[success skipped canceled failed running pending].each do |status| + .ci_widget{class: "ci-#{status}", style: "display:none"} = ci_icon_for_status(status) %span CI build @@ -16,20 +28,22 @@ = succeed "." do = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage - - if details_path = builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + - if details_path = ci_build_details_path(@merge_request) = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink" - - if @merge_request.has_ci? - - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # Remove in later versions when services like Jenkins will set CI status via Commit status API - .ci_widget.ci-not_found{style: "display:none"} - = icon("times-circle") - Could not find CI status for #{@merge_request.last_commit_short_sha}. - .ci_widget.ci-error{style: "display:none"} - = icon("times-circle") - Could not connect to the CI server. Please check your settings and try again. + .ci_widget + = icon("spinner spin") + Checking CI status for #{@merge_request.last_commit_short_sha}… - :javascript - $(function() { - merge_request_widget.getCIState(); - }); + .ci_widget.ci-not_found{style: "display:none"} + = icon("times-circle") + Could not find CI status for #{@merge_request.last_commit_short_sha}. + + .ci_widget.ci-error{style: "display:none"} + = icon("times-circle") + Could not connect to the CI server. Please check your settings and try again. + + :javascript + $(function() { + merge_request_widget.getCIStatus(false); + }); From c613b8c6dcdcff4d29e9853f1e6654343e212400 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 21 Mar 2016 09:54:13 +0000 Subject: [PATCH 056/264] Put back hiding of modal --- app/assets/javascripts/merge_request_widget.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 43671ee3939..7102a0673e9 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -7,6 +7,7 @@ class @MergeRequestWidget # constructor: (@opts) -> + $('#modal_merge_info').modal(show: false) @firstCICheck = true @readyForCICheck = true clearInterval @fetchBuildStatusInterval From 80174538a8e6a0a1629e65cbd94782ab4f6ccc61 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 21 Mar 2016 12:31:21 -0500 Subject: [PATCH 057/264] Use .empty() --- app/assets/javascripts/search_autocomplete.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 25db343ca46..e99a221222e 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -176,7 +176,7 @@ class @SearchAutocomplete if @originalState._location is '' - @locationBadgeEl.html('') + @locationBadgeEl.empty() else @addLocationBadge( value: @originalState._location From 6e9ff2e5745db44c23127425a6d9ab122643d78b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 21 Mar 2016 13:18:34 -0500 Subject: [PATCH 058/264] Delete .remove-badge from badge --- app/assets/javascripts/search_autocomplete.js.coffee | 1 - app/views/shared/_location_badge.html.haml | 3 --- 2 files changed, 4 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index e99a221222e..69ca0d996d1 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -164,7 +164,6 @@ class @SearchAutocomplete html = " #{category}#{value} - x " @locationBadgeEl.html(html) diff --git a/app/views/shared/_location_badge.html.haml b/app/views/shared/_location_badge.html.haml index f1ecc060cf1..489c0e11d0b 100644 --- a/app/views/shared/_location_badge.html.haml +++ b/app/views/shared/_location_badge.html.haml @@ -7,6 +7,3 @@ %span.location-badge %i.location-text = label - - %a.remove-badge{href: '#'} - x From f7a97291c02f9eeb6d1d7ffc69526a5750c716f6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 21 Mar 2016 13:29:31 -0500 Subject: [PATCH 059/264] Add variables --- app/assets/stylesheets/framework/variables.scss | 8 ++++++++ app/assets/stylesheets/pages/search.scss | 13 +++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index be626678bd7..9d820a46cfb 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -192,3 +192,11 @@ $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; $award-emoji-menu-bg: #fff; $award-emoji-menu-border: #f1f2f4; $award-emoji-new-btn-icon-color: #dcdcdc; + +/* + * Search Box + */ +$location-badge-color: #aaa; +$location-badge-bg: $gray-normal; +$location-icon-color: #e7e9ed; + diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 4a02f75719b..110258a9e11 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -56,10 +56,6 @@ font-style: normal; } - .remove-badge { - display: none; - } - .search-input { border: none; font-size: 14px; @@ -73,16 +69,17 @@ .location-badge { line-height: 25px; padding: 0 5px; - border-radius: 2px; + border-radius: $border-radius-default; font-size: 14px; font-style: normal; - color: #AAAAAA; + color: $location-badge-color; display: inline-block; - background-color: #F5F5F5; + background-color: $location-badge-bg; vertical-align: top; } .search-input-container { + display: -webkit-flex; display: flex; } @@ -100,7 +97,7 @@ @include transition(color .15s); position: absolute; right: 5px; - color: #E7E9ED; + color: $location-icon-color; top: 0; -webkit-user-select: none; -moz-user-select: none; From 3a8c4ebb43616abaad1087c8384a61138a6398dd Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 21 Mar 2016 14:01:20 -0500 Subject: [PATCH 060/264] Loop through form inputs --- .../javascripts/search_autocomplete.js.coffee | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 69ca0d996d1..8165502914e 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -28,7 +28,6 @@ class @SearchAutocomplete @groupInputEl = @getElement('#group_id') @searchCodeInputEl = @getElement('#search_code') @repositoryInputEl = @getElement('#repository_ref') - @scopeInputEl = @getElement('#scope') @saveOriginalState() @@ -51,6 +50,7 @@ class @SearchAutocomplete group_id: @groupInputEl.val() search_code: @searchCodeInputEl.val() repository_ref: @repositoryInputEl.val() + scope: @scopeInputEl.val() # Location badge _location: $.trim(@locationText.text()) @@ -191,20 +191,17 @@ class @SearchAutocomplete @locationBadgeEl.children().length resetSearchState: -> - # Remove scope - @scopeInputEl.val('') + inputs = Object.keys @originalState - # Remove group - @groupInputEl.val('') + for input in inputs - # Remove project id - @projectInputEl.val('') + # _location isnt a input + break if input is '_location' - # Remove code search - @searchCodeInputEl.val('') + # renamed to avoid tests to fail + if input is 'project_id' then input = 'search_project_id' - # Remove repository ref - @repositoryInputEl.val('') + @getElement("##{input}").val('') removeLocationBadge: -> @locationBadgeEl.empty() From eff98ffe05d210a113a0b00aa0104911eaa90fa1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 21 Mar 2016 14:23:29 -0500 Subject: [PATCH 061/264] TAB is not used --- app/assets/javascripts/search_autocomplete.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 8165502914e..df6cb4f2c18 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -3,7 +3,6 @@ class @SearchAutocomplete KEYCODE = ESCAPE: 27 BACKSPACE: 8 - TAB: 9 ENTER: 13 constructor: (opts = {}) -> From a477d604f635a02e067e9b051866af534ed0fb5b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 21 Mar 2016 16:00:53 -0500 Subject: [PATCH 062/264] Add ability to clear location badge --- .../javascripts/search_autocomplete.js.coffee | 28 ++++++-- .../stylesheets/framework/variables.scss | 3 +- app/assets/stylesheets/pages/search.scss | 71 ++++++++++++------- app/views/layouts/_search.html.haml | 13 +++- app/views/shared/_location_badge.html.haml | 9 --- 5 files changed, 82 insertions(+), 42 deletions(-) delete mode 100644 app/views/shared/_location_badge.html.haml diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index df6cb4f2c18..fc8595f60c3 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -27,6 +27,7 @@ class @SearchAutocomplete @groupInputEl = @getElement('#group_id') @searchCodeInputEl = @getElement('#search_code') @repositoryInputEl = @getElement('#repository_ref') + @clearInput = @getElement('.js-clear-input') @saveOriginalState() @@ -59,6 +60,7 @@ class @SearchAutocomplete @searchInput.on 'keydown', @onSearchInputKeyDown @searchInput.on 'focus', @onSearchInputFocus @searchInput.on 'blur', @onSearchInputBlur + @clearInput.on 'click', @onRemoveLocationClick enableAutocomplete: -> return if @autocomplete @@ -150,12 +152,25 @@ class @SearchAutocomplete onSearchInputFocus: => @wrap.addClass('search-active') - onSearchInputBlur: => - @wrap.removeClass('search-active') + onRemoveLocationClick: (e) => + e.preventDefault() + @removeLocationBadge() + @searchInput.val('').focus() + @skipBlurEvent = true - # If input is blank then restore state - if @searchInput.val() is '' - @restoreOriginalState() + onSearchInputBlur: (e) => + @skipBlurEvent = false + + # We should wait to make sure we are not clearing the input instead + setTimeout( => + return if @skipBlurEvent + + @wrap.removeClass('search-active') + + # If input is blank then restore state + if @searchInput.val() is '' + @restoreOriginalState() + , 100) addLocationBadge: (item) -> category = if item.category? then "#{item.category}: " else '' @@ -165,6 +180,7 @@ class @SearchAutocomplete #{category}#{value} " @locationBadgeEl.html(html) + @wrap.addClass('has-location-badge') restoreOriginalState: -> inputs = Object.keys @originalState @@ -208,6 +224,8 @@ class @SearchAutocomplete # Reset state @resetSearchState() + @wrap.removeClass('has-location-badge') + disableAutocomplete: -> if @autocomplete @searchInput.addClass('disabled') diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 9d820a46cfb..8f260f24c4c 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -199,4 +199,5 @@ $award-emoji-new-btn-icon-color: #dcdcdc; $location-badge-color: #aaa; $location-badge-bg: $gray-normal; $location-icon-color: #e7e9ed; - +$location-active-color: #7f8fa4; +$location-active-bg: $location-active-color; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 110258a9e11..4179d0adb3e 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -26,24 +26,6 @@ margin-left: 10px; margin-top: ($header-height - 35) / 2; - &.search-active { - form { - @extend .form-control:focus; - } - - .location-badge { - @include transition(all .15s); - background-color: $input-border-focus; - color: $white-light; - } - - .search-input-wrap { - i { - color: $input-border-focus; - } - } - } - form { @extend .form-control; margin: 0; @@ -92,16 +74,11 @@ width: 100%; position: relative; - .search-icon { - @extend .fa-search; - @include transition(color .15s); + .search-icon, .clear-icon { position: absolute; right: 5px; - color: $location-icon-color; top: 0; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + color: $location-icon-color; &:before { font-family: FontAwesome; @@ -110,9 +87,53 @@ } } + .search-icon { + @extend .fa-search; + @include transition(color .15s); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + } + + .clear-icon { + @extend .fa-times; + display: none; + } + .dropdown-header { text-transform: uppercase; font-size: 11px; } } + + &.search-active { + form { + @extend .form-control:focus; + } + + .location-badge { + @include transition(all .15s); + background-color: $location-active-bg; + color: $white-light; + } + + .search-input-wrap { + i { + color: $location-active-color; + } + } + + &.has-location-badge { + .search-icon { + display: none; + } + + .clear-icon { + cursor: pointer; + display: block; + } + } + } + + } diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index f051e7a1867..0a5c145029b 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,8 +1,16 @@ -.search.search-form +- if controller.controller_path =~ /^groups/ + - label = 'This group' +- if controller.controller_path =~ /^projects/ + - label = 'This project' + +.search.search-form{class: "#{'has-location-badge' if label.present?}"} = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container .search-location-badge - = render 'shared/location_badge' + - if label.present? + %span.location-badge + %i.location-text + = label .search-input-wrap .dropdown{ data: {url: search_autocomplete_path } } = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } @@ -10,6 +18,7 @@ = dropdown_content = dropdown_loading %i.search-icon + %i.clear-icon.js-clear-input = hidden_field_tag :group_id, @group.try(:id) = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id' diff --git a/app/views/shared/_location_badge.html.haml b/app/views/shared/_location_badge.html.haml deleted file mode 100644 index 489c0e11d0b..00000000000 --- a/app/views/shared/_location_badge.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- if controller.controller_path =~ /^groups/ - - label = 'This group' -- if controller.controller_path =~ /^projects/ - - label = 'This project' - -- if label.present? - %span.location-badge - %i.location-text - = label From 521c0f5f08fb0e58f3bb648d6469496e4ac96c48 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 21 Mar 2016 23:01:19 -0500 Subject: [PATCH 063/264] Reduce the use of loops --- .../javascripts/search_autocomplete.js.coffee | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index fc8595f60c3..a8ae261c4d2 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -97,25 +97,20 @@ class @SearchAutocomplete }, (response) -> data = [] - # Save groups ordering according to server response - groupNames = _.unique(_.pluck(response, 'category')) - - # Group results by category name - groups = _.groupBy response, (item) -> - item.category - # List results - for groupName in groupNames + for suggestion in response # Add group header before list each group - data.push - header: groupName - - # List group - for item in groups[groupName] + if lastCategory isnt suggestion.category data.push - text: item.label - url: item.url + header: suggestion.category + + lastCategory = suggestion.category + + data.push + text: suggestion.label + url: suggestion.url + callback(data) ).always -> loading = false From 4fcd7ba954d6f2e0c80cd3b2005f7a74fd5bbc90 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 21 Mar 2016 23:01:42 -0500 Subject: [PATCH 064/264] Set constants for category names --- app/helpers/search_helper.rb | 59 +++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index de164547396..e6aa21887ec 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,4 +1,11 @@ module SearchHelper + + CAT_SETTINGS = 'Settings' + CAT_HELP = 'Help' + CAT_CURR_PROJECT = 'Current Project' + CAT_GROUPS = 'Groups' + CAT_PROJECTS = 'Projects' + def search_autocomplete_opts(term) return unless current_user @@ -23,25 +30,25 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { category: "Settings", label: "Profile settings", url: profile_path }, - { category: "Settings", label: "SSH Keys", url: profile_keys_path }, - { category: "Settings", label: "Dashboard", url: root_path }, - { category: "Settings", label: "Admin Section", url: admin_root_path }, + { category: CAT_SETTINGS, label: "Profile settings", url: profile_path }, + { category: CAT_SETTINGS, label: "SSH Keys", url: profile_keys_path }, + { category: CAT_SETTINGS, label: "Dashboard", url: root_path }, + { category: CAT_SETTINGS, label: "Admin Section", url: admin_root_path }, ] end # Autocomplete results for internal help pages def help_autocomplete [ - { category: "Help", label: "API Help", url: help_page_path("api", "README") }, - { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") }, - { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") }, - { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") }, - { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") }, - { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") }, - { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, - { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, - { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") }, + { category: CAT_HELP, label: "API Help", url: help_page_path("api", "README") }, + { category: CAT_HELP, label: "Markdown Help", url: help_page_path("markdown", "markdown") }, + { category: CAT_HELP, label: "Permissions Help", url: help_page_path("permissions", "permissions") }, + { category: CAT_HELP, label: "Public Access Help", url: help_page_path("public_access", "public_access") }, + { category: CAT_HELP, label: "Rake Tasks Help", url: help_page_path("raketasks", "README") }, + { category: CAT_HELP, label: "SSH Keys Help", url: help_page_path("ssh", "README") }, + { category: CAT_HELP, label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") }, + { category: CAT_HELP, label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") }, + { category: CAT_HELP, label: "Workflow Help", url: help_page_path("workflow", "README") }, ] end @@ -51,16 +58,16 @@ module SearchHelper ref = @ref || @project.repository.root_ref [ - { category: "Current Project", label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, - { category: "Current Project", label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, - { category: "Current Project", label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, - { category: "Current Project", label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, - { category: "Current Project", label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, - { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, - { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, - { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, - { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, + { category: CAT_CURR_PROJECT, label: "Files", url: namespace_project_tree_path(@project.namespace, @project, ref) }, + { category: CAT_CURR_PROJECT, label: "Commits", url: namespace_project_commits_path(@project.namespace, @project, ref) }, + { category: CAT_CURR_PROJECT, label: "Network", url: namespace_project_network_path(@project.namespace, @project, ref) }, + { category: CAT_CURR_PROJECT, label: "Graph", url: namespace_project_graph_path(@project.namespace, @project, ref) }, + { category: CAT_CURR_PROJECT, label: "Issues", url: namespace_project_issues_path(@project.namespace, @project) }, + { category: CAT_CURR_PROJECT, label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, + { category: CAT_CURR_PROJECT, label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, + { category: CAT_CURR_PROJECT, label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, + { category: CAT_CURR_PROJECT, label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: CAT_CURR_PROJECT, label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else [] @@ -71,8 +78,7 @@ module SearchHelper def groups_autocomplete(term, limit = 5) current_user.authorized_groups.search(term).limit(limit).map do |group| { - category: "Groups", - location: "groups", + category: CAT_GROUPS, id: group.id, label: "#{search_result_sanitize(group.name)}", url: group_path(group) @@ -85,8 +91,7 @@ module SearchHelper current_user.authorized_projects.search_by_title(term). sorted_by_stars.non_archived.limit(limit).map do |p| { - category: "Projects", - location: "projects", + category: CAT_PROJECTS, id: p.id, value: "#{search_result_sanitize(p.name)}", label: "#{search_result_sanitize(p.name_with_namespace)}", From fa4126acffdfe13741e05a60ad5ed7fd407b4f16 Mon Sep 17 00:00:00 2001 From: Baldinof Date: Tue, 22 Mar 2016 15:34:35 +0100 Subject: [PATCH 065/264] Move unlink fork logic to a service --- app/controllers/projects_controller.rb | 2 +- app/models/project.rb | 20 ------------ app/services/projects/unlink_fork_service.rb | 19 +++++++++++ spec/models/project_spec.rb | 19 ----------- .../projects/unlink_fork_service_spec.rb | 32 +++++++++++++++++++ 5 files changed, 52 insertions(+), 40 deletions(-) create mode 100644 app/services/projects/unlink_fork_service.rb create mode 100644 spec/services/projects/unlink_fork_service_spec.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 94789702d65..87657e4e3d2 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -71,7 +71,7 @@ class ProjectsController < ApplicationController def remove_fork return access_denied! unless can?(current_user, :remove_fork_project, @project) - if @project.unlink_fork(current_user) + if ::Projects::UnlinkForkService.new(@project, current_user).execute flash[:notice] = 'The fork relationship has been removed.' end end diff --git a/app/models/project.rb b/app/models/project.rb index 8d9908128e2..691b706ea40 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -917,26 +917,6 @@ class Project < ActiveRecord::Base self.builds_enabled = true end - def unlink_fork(user) - if forked? - forked_from_project.lfs_objects.find_each do |lfs_object| - lfs_object.projects << self - end - - merge_requests = forked_from_project.merge_requests.opened.from_project(self) - - unless merge_requests.empty? - close_service = MergeRequests::CloseService.new(self, user) - - merge_requests.each do |mr| - close_service.execute(mr) - end - end - - forked_project_link.destroy - end - end - def any_runners?(&block) if runners.active.any?(&block) return true diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb new file mode 100644 index 00000000000..d0703effa1d --- /dev/null +++ b/app/services/projects/unlink_fork_service.rb @@ -0,0 +1,19 @@ +module Projects + class UnlinkForkService < BaseService + def execute + return unless @project.forked? + + @project.forked_from_project.lfs_objects.find_each do |lfs_object| + lfs_object.projects << self + end + + merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) + + merge_requests.each do |mr| + MergeRequests::CloseService.new(@project, @current_user).execute(mr) + end + + @project.forked_project_link.destroy + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1ca78daa5b3..59c5ffa6b9c 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -699,25 +699,6 @@ describe Project, models: true do end end - describe '#unlink_fork' do - let(:fork_link) { create(:forked_project_link) } - let(:fork_project) { fork_link.forked_to_project } - let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) } - let!(:close_service) { MergeRequests::CloseService.new(fork_project, user) } - - it 'remove fork relation and close all pending merge requests' do - allow(MergeRequests::CloseService).to receive(:new). - with(fork_project, user). - and_return(close_service) - - expect(close_service).to receive(:execute).with(merge_request) - expect(fork_project.forked_project_link).to receive(:destroy) - - fork_project.unlink_fork(user) - end - end - describe '.search_by_title' do let(:project) { create(:project, name: 'kittens') } diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb new file mode 100644 index 00000000000..f287b0a59b2 --- /dev/null +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Projects::UnlinkForkService, services: true do + subject { Projects::UnlinkForkService.new(fork_project, user) } + + let(:fork_link) { create(:forked_project_link) } + let(:fork_project) { fork_link.forked_to_project } + let(:user) { create(:user) } + + context 'with opened merge request on the source project' do + let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) } + let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) } + + before do + allow(MergeRequests::CloseService).to receive(:new). + with(fork_project, user). + and_return(mr_close_service) + end + + it 'close all pending merge requests' do + expect(mr_close_service).to receive(:execute).with(merge_request) + + subject.execute + end + end + + it 'remove fork relation' do + expect(fork_project.forked_project_link).to receive(:destroy) + + subject.execute + end +end From b25d42cee9db265afe15d8ef192490a1ce2e3471 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Mon, 21 Mar 2016 21:25:15 -0500 Subject: [PATCH 066/264] Update LDAP docs [ci skip] --- doc/README.md | 4 +- doc/administration/auth/README.md | 11 ++ doc/administration/auth/ldap.md | 277 ++++++++++++++++++++++++++++++ doc/integration/ldap.md | 227 +----------------------- 4 files changed, 292 insertions(+), 227 deletions(-) create mode 100644 doc/administration/auth/README.md create mode 100644 doc/administration/auth/ldap.md diff --git a/doc/README.md b/doc/README.md index 08d0a6a5bfb..739b7e10194 100644 --- a/doc/README.md +++ b/doc/README.md @@ -19,10 +19,12 @@ ## Administrator documentation +- [Authentication/Authorization](administration/auth/README.md) Configure + external authentication with LDAP, SAML, CAS and additional Omniauth providers. - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough. - [Install](install/README.md) Requirements, directory structures and installation from source. - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components -- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter. +- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. - [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. - [Log system](logs/logs.md) Log system. diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md new file mode 100644 index 00000000000..07e548aaabe --- /dev/null +++ b/doc/administration/auth/README.md @@ -0,0 +1,11 @@ +# Authentication and Authorization + +GitLab integrates with the following external authentication and authorization +providers. + +- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, + and 389 Server +- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, + Bitbucket, Facebook, Shibboleth, Crowd and Azure +- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md new file mode 100644 index 00000000000..237700bbcd9 --- /dev/null +++ b/doc/administration/auth/ldap.md @@ -0,0 +1,277 @@ +# LDAP + +GitLab integrates with LDAP to support user authentication. +This integration works with most LDAP-compliant directory +servers, including Microsoft Active Directory, Apple Open Directory, Open LDAP, +and 389 Server. GitLab EE includes enhanced integration, including group +membership syncing. + +## Security + +GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' +or 'userPrincipalName' attribute. An LDAP user who is allowed to change their +email on the LDAP server can potentially +[take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) +on your GitLab server. + +We recommend against using LDAP integration if your LDAP users are +allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on +the LDAP server. + +### User deletion + +If a user is deleted from the LDAP server, they will be blocked in GitLab, as +well. Users will be immediately blocked from logging in. However, there is an +LDAP check cache time (sync time) of one hour (see note). This means users that +are already logged in or are using Git over SSH will still be able to access +GitLab for up to one hour. Manually block the user in the GitLab Admin area to +immediately block all access. + +>**Note**: GitLab EE supports a configurable sync time, with a default +of one hour. + +## Configuration + +To enable LDAP integration you need to add your LDAP server settings in +`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. + +>**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to +one GitLab server. + +Prior to version 7.4, GitLab used a different syntax for configuring +LDAP integration. The old LDAP integration syntax still works but may be +removed in a future version. If your `gitlab.rb` or `gitlab.yml` file contains +LDAP settings in both the old syntax and the new syntax, only the __old__ +syntax will be used by GitLab. + +The configuration inside `gitlab_rails['ldap_servers']` below is sensitive to +incorrect indentation. Be sure to retain the indentation given in the example. +Copy/paste can sometimes cause problems. + +**Omnibus configuration** + +```ruby +gitlab_rails['ldap_enabled'] = true +gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below +main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + + host: '_your_ldap_server' + port: 389 + uid: 'sAMAccountName' + method: 'plain' # "tls" or "ssl" or "plain" + bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' + password: '_the_password_of_the_bind_user' + + # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking + # a request if the LDAP server becomes unresponsive. + # A value of 0 means there is no timeout. + timeout: 10 + + # This setting specifies if LDAP server is Active Directory LDAP server. + # For non AD servers it skips the AD specific queries. + # If your LDAP server is not AD, set this to false. + active_directory: true + + # If allow_username_or_email_login is enabled, GitLab will ignore everything + # after the first '@' in the LDAP username submitted by the user on login. + # + # Example: + # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; + # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. + # + # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to + # disable this setting, because the userPrincipalName contains an '@'. + allow_username_or_email_login: false + + # To maintain tight control over the number of active users on your GitLab installation, + # enable this setting to keep new users blocked until they have been cleared by the admin + # (default: false). + block_auto_created_users: false + + # Base where we can search for users + # + # Ex. ou=People,dc=gitlab,dc=example + # + base: '' + + # Filter LDAP users + # + # Format: RFC 4515 https://tools.ietf.org/search/rfc4515 + # Ex. (employeeType=developer) + # + # Note: GitLab does not support omniauth-ldap's custom filter syntax. + # + user_filter: '' + + # LDAP attributes that GitLab will use to create an account for the LDAP user. + # The specified attribute can either be the attribute name as a string (e.g. 'mail'), + # or an array of attribute names to try in order (e.g. ['mail', 'email']). + # Note that the user's LDAP login will always be the attribute specified as `uid` above. + attributes: + # The username will be used in paths for the user's own projects + # (like `gitlab.example.com/username/project`) and when mentioning + # them in issues, merge request and comments (like `@username`). + # If the attribute specified for `username` contains an email address, + # the GitLab username will be the part of the email address before the '@'. + username: ['uid', 'userid', 'sAMAccountName'] + email: ['mail', 'email', 'userPrincipalName'] + + # If no full name could be found at the attribute specified for `name`, + # the full name is determined using the attributes specified for + # `first_name` and `last_name`. + name: 'cn' + first_name: 'givenName' + last_name: 'sn' + + ## EE only + + # Base where we can search for groups + # + # Ex. ou=groups,dc=gitlab,dc=example + # + group_base: '' + + # The CN of a group containing GitLab administrators + # + # Ex. administrators + # + # Note: Not `cn=administrators` or the full DN + # + admin_group: '' + + # The LDAP attribute containing a user's public SSH key + # + # Ex. ssh_public_key + # + sync_ssh_keys: false + +# GitLab EE only: add more LDAP servers +# Choose an ID made of a-z and 0-9 . This ID will be stored in the database +# so that GitLab can remember which LDAP server a user belongs to. +# uswest2: +# label: +# host: +# .... +EOS +``` + +**Source configuration** + +Use the same format as `gitlab_rails['ldap_servers']` for the contents under +`servers:` in the example below: + +``` +production: + # snip... + ldap: + enabled: false + servers: + main: # 'main' is the GitLab 'provider ID' of this LDAP server + ## label + # + # A human-friendly name for your LDAP server. It is OK to change the label later, + # for instance if you find out it is too large to fit on the web page. + # + # Example: 'Paris' or 'Acme, Ltd.' + label: 'LDAP' + # snip... +``` + +## Using an LDAP filter to limit access to your GitLab server + +If you want to limit all GitLab access to a subset of the LDAP users on your +LDAP server, the first step should be to narrow the configured `base`. However, +it is sometimes necessary to filter users further. In this case, you can set up +an LDAP user filter. The filter must comply with +[RFC 4515](https://tools.ietf.org/search/rfc4515). + +**Omnibus configuration** + +```ruby +gitlab_rails['ldap_servers'] = YAML.load <<-EOS +main: + # snip... + user_filter: '(employeeType=developer)' +EOS +``` + +**Source configuration** + +```yaml +production: + ldap: + servers: + main: + # snip... + user_filter: '(employeeType=developer)' +``` + +Tip: If you want to limit access to the nested members of an Active Directory +group you can use the following syntax: + +``` +(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com) +``` + +Please note that GitLab does not support the custom filter syntax used by +omniauth-ldap. + +## Enabling LDAP sign-in for existing GitLab users + +When a user signs in to GitLab with LDAP for the first time, and their LDAP +email address is the primary email address of an existing GitLab user, then +the LDAP DN will be associated with the existing user. If the LDAP email +attribute is not found in GitLab's database, a new user is created. + +In other words, if an existing GitLab user wants to enable LDAP sign-in for +themselves, they should check that their GitLab email address matches their +LDAP email address, and then sign into GitLab via their LDAP credentials. + +## Limitations + +### TLS Client Authentication + +Not implemented by `Net::LDAP`. +You should disable anonymous LDAP authentication and enable simple or SASL +authentication. The TLS client authentication setting in your LDAP server cannot +be mandatory and clients cannot be authenticated with the TLS protocol. + +### TLS Server Authentication + +Not supported by GitLab's configuration options. +When setting `method: ssl`, the underlying authentication method used by +`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with +the LDAP server before any LDAP-protocol data is exchanged but no validation of +the LDAP server's SSL certificate is performed. + +## Troubleshooting + +### Invalid credentials when logging in + +- Make sure the user you are binding with has enough permissions to read the user's +tree and traverse it. +- Check that the `user_filter` is not blocking otherwise valid users. +- Run the following check command to make sure that the LDAP settings are + correct and GitLab can see your users: + + ```bash + # For Omnibus installations + sudo gitlab-rake gitlab:ldap:check + + # For installations from source + sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production + ``` + +### Connection Refused + +If you are getting 'Connection Refused' errors when trying to connect to the +LDAP server please double-check the LDAP `port` and `method` settings used by +GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR +`method: 'ssl'` and `port: 636`. diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index cf1f98492ea..fb20308c49c 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -1,228 +1,3 @@ # GitLab LDAP integration -GitLab can be configured to allow your users to sign with their LDAP credentials to integrate with e.g. Active Directory. - -The first time a user signs in with LDAP credentials, GitLab will create a new GitLab user associated with the LDAP Distinguished Name (DN) of the LDAP user. - -GitLab user attributes such as nickname and email will be copied from the LDAP user entry. - -## Security - -GitLab assumes that LDAP users are not able to change their LDAP 'mail', 'email' or 'userPrincipalName' attribute. -An LDAP user who is allowed to change their email on the LDAP server can [take over any account](#enabling-ldap-sign-in-for-existing-gitlab-users) on your GitLab server. - -We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server. - -If a user is deleted from the LDAP server, they will be blocked in GitLab as well. -Users will be immediately blocked from logging in. However, there is an LDAP check -cache time of one hour. The means users that are already logged in or are using Git -over SSH will still be able to access GitLab for up to one hour. Manually block -the user in the GitLab Admin area to immediately block all access. - -## Configuring GitLab for LDAP integration - -To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`. -In GitLab Enterprise Edition you can have multiple LDAP servers connected to one GitLab server. - -Please note that before version 7.4, GitLab used a different syntax for configuring LDAP integration. -The old LDAP integration syntax still works in GitLab 7.4. -If your `gitlab.rb` or `gitlab.yml` file contains LDAP settings in both the old syntax and the new syntax, only the __old__ syntax will be used by GitLab. - -```ruby -# For omnibus packages -gitlab_rails['ldap_enabled'] = true -gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below -main: # 'main' is the GitLab 'provider ID' of this LDAP server - ## label - # - # A human-friendly name for your LDAP server. It is OK to change the label later, - # for instance if you find out it is too large to fit on the web page. - # - # Example: 'Paris' or 'Acme, Ltd.' - label: 'LDAP' - - host: '_your_ldap_server' - port: 389 - uid: 'sAMAccountName' - method: 'plain' # "tls" or "ssl" or "plain" - bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' - password: '_the_password_of_the_bind_user' - - # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking - # a request if the LDAP server becomes unresponsive. - # A value of 0 means there is no timeout. - timeout: 10 - - # This setting specifies if LDAP server is Active Directory LDAP server. - # For non AD servers it skips the AD specific queries. - # If your LDAP server is not AD, set this to false. - active_directory: true - - # If allow_username_or_email_login is enabled, GitLab will ignore everything - # after the first '@' in the LDAP username submitted by the user on login. - # - # Example: - # - the user enters 'jane.doe@example.com' and 'p@ssw0rd' as LDAP credentials; - # - GitLab queries the LDAP server with 'jane.doe' and 'p@ssw0rd'. - # - # If you are using "uid: 'userPrincipalName'" on ActiveDirectory you need to - # disable this setting, because the userPrincipalName contains an '@'. - allow_username_or_email_login: false - - # To maintain tight control over the number of active users on your GitLab installation, - # enable this setting to keep new users blocked until they have been cleared by the admin - # (default: false). - block_auto_created_users: false - - # Base where we can search for users - # - # Ex. ou=People,dc=gitlab,dc=example - # - base: '' - - # Filter LDAP users - # - # Format: RFC 4515 https://tools.ietf.org/search/rfc4515 - # Ex. (employeeType=developer) - # - # Note: GitLab does not support omniauth-ldap's custom filter syntax. - # - user_filter: '' - - # LDAP attributes that GitLab will use to create an account for the LDAP user. - # The specified attribute can either be the attribute name as a string (e.g. 'mail'), - # or an array of attribute names to try in order (e.g. ['mail', 'email']). - # Note that the user's LDAP login will always be the attribute specified as `uid` above. - attributes: - # The username will be used in paths for the user's own projects - # (like `gitlab.example.com/username/project`) and when mentioning - # them in issues, merge request and comments (like `@username`). - # If the attribute specified for `username` contains an email address, - # the GitLab username will be the part of the email address before the '@'. - username: ['uid', 'userid', 'sAMAccountName'] - email: ['mail', 'email', 'userPrincipalName'] - - # If no full name could be found at the attribute specified for `name`, - # the full name is determined using the attributes specified for - # `first_name` and `last_name`. - name: 'cn' - first_name: 'givenName' - last_name: 'sn' - -# GitLab EE only: add more LDAP servers -# Choose an ID made of a-z and 0-9 . This ID will be stored in the database -# so that GitLab can remember which LDAP server a user belongs to. -# uswest2: -# label: -# host: -# .... -EOS -``` - -If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `method` settings used by GitLab. -Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`. - -If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`: - -``` -production: - # snip... - ldap: - enabled: false - servers: - main: # 'main' is the GitLab 'provider ID' of this LDAP server - ## label - # - # A human-friendly name for your LDAP server. It is OK to change the label later, - # for instance if you find out it is too large to fit on the web page. - # - # Example: 'Paris' or 'Acme, Ltd.' - label: 'LDAP' - # snip... -``` - -## Enabling LDAP sign-in for existing GitLab users - -When a user signs in to GitLab with LDAP for the first time, and their LDAP email address is the primary email address of an existing GitLab user, then the LDAP DN will be associated with the existing user. - -If the LDAP email attribute is not found in GitLab's database, a new user is created. - -In other words, if an existing GitLab user wants to enable LDAP sign-in for themselves, they should check that their GitLab email address matches their LDAP email address, and then sign into GitLab via their LDAP credentials. - -GitLab recognizes the following LDAP attributes as email addresses: `mail`, `email` and `userPrincipalName`. - -If multiple LDAP email attributes are present, e.g. `mail: foo@bar.com` and `email: foo@example.com`, then the first attribute found wins -- in this case `foo@bar.com`. - -## Using an LDAP filter to limit access to your GitLab server - -If you want to limit all GitLab access to a subset of the LDAP users on your LDAP server you can set up an LDAP user filter. -The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515). - -```ruby -# For omnibus packages; new LDAP server syntax -gitlab_rails['ldap_servers'] = YAML.load <<-EOS -main: - # snip... - user_filter: '(employeeType=developer)' -EOS -``` - -```yaml -# For installations from source; new LDAP server syntax -production: - ldap: - servers: - main: - # snip... - user_filter: '(employeeType=developer)' -``` - -Tip: if you want to limit access to the nested members of an Active Directory group you can use the following syntax: - -``` -(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com) -``` - -Please note that GitLab does not support the custom filter syntax used by omniauth-ldap. - -## Limitations - -GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap) -which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation -of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP -server settings. - -### TLS Client Authentication -Not implemented by `Net::LDAP`. -So you should disable anonymous LDAP authentication and enable simple or SASL -authentication. TLS client authentication setting in your LDAP server cannot be -mandatory and clients cannot be authenticated with the TLS protocol. - -### TLS Server Authentication -Not supported by GitLab's configuration options. -When setting `method: ssl`, the underlying authentication method used by -`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with -the LDAP server before any LDAP-protocol data is exchanged but no validation of -the LDAP server's SSL certificate is performed. - -## Troubleshooting - -### Invalid credentials when logging in - -Make sure the user you are binding with has enough permissions to read the user's -tree and traverse it. - -Also make sure that the `user_filter` is not blocking otherwise valid users. - -To make sure that the LDAP settings are correct and GitLab can see your users, -execute the following command: - - -```bash -# For Omnibus installations -sudo gitlab-rake gitlab:ldap:check - -# For installations from source -sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production -``` - +This document was moved under [`administration/auth/ldap`](administration/auth/ldap.md). From d05ec645aef1bd3529902dd6f5e527f9795f1199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Manh=C3=A3es?= Date: Tue, 22 Mar 2016 20:55:19 -0300 Subject: [PATCH 067/264] Fix order of steps to prevent PostgreSQL errors when running migration [ci skip] --- doc/update/8.5-to-8.6.md | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md index 712e9fdf93a..b9abcbd2c12 100644 --- a/doc/update/8.5-to-8.6.md +++ b/doc/update/8.5-to-8.6.md @@ -62,7 +62,26 @@ sudo -u git -H git checkout v0.7.1 sudo -u git -H make ``` -### 6. Install libs, migrations, etc. +### 6. Updates for PostgreSQL Users + +Starting with 8.6 users using GitLab in combination with PostgreSQL are required +to have the `pg_trgm` extension enabled for all GitLab databases. If you're +using GitLab's Omnibus packages there's nothing you'll need to do manually as +this extension is enabled automatically. Users who install GitLab without using +Omnibus (e.g. by building from source) have to enable this extension manually. +To enable this extension run the following SQL command as a PostgreSQL super +user for _every_ GitLab database: + +```sql +CREATE EXTENSION IF NOT EXISTS pg_trgm; +``` + +Certain operating systems might require the installation of extra packages for +this extension to be available. For example, users using Ubuntu will have to +install the `postgresql-contrib` package in order for this extension to be +available. + +### 7. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -84,7 +103,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ``` -### 7. Update configuration files +### 8. Update configuration files #### New configuration options for `gitlab.yml` @@ -120,25 +139,6 @@ Ensure you're still up-to-date with the latest init script changes: sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab -### 8. Updates for PostgreSQL Users - -Starting with 8.6 users using GitLab in combination with PostgreSQL are required -to have the `pg_trgm` extension enabled for all GitLab databases. If you're -using GitLab's Omnibus packages there's nothing you'll need to do manually as -this extension is enabled automatically. Users who install GitLab without using -Omnibus (e.g. by building from source) have to enable this extension manually. -To enable this extension run the following SQL command as a PostgreSQL super -user for _every_ GitLab database: - -```sql -CREATE EXTENSION IF NOT EXISTS pg_trgm; -``` - -Certain operating systems might require the installation of extra packages for -this extension to be available. For example, users using Ubuntu will have to -install the `postgresql-contrib` package in order for this extension to be -available. - ### 9. Start application sudo service gitlab start From 44817726fe52a5a061396a2280f7fd19c7d494d0 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 23 Mar 2016 14:12:33 -0500 Subject: [PATCH 068/264] Fixes empty menu when typing on search input for the very first time --- .../javascripts/search_autocomplete.js.coffee | 115 ++++++++++-------- app/views/layouts/_search.html.haml | 5 +- 2 files changed, 69 insertions(+), 51 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index a8ae261c4d2..a06c80b60cd 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -31,6 +31,8 @@ class @SearchAutocomplete @saveOriginalState() + @createAutocomplete() + @searchInput.addClass('disabled') @autocomplete = false @@ -43,6 +45,57 @@ class @SearchAutocomplete saveOriginalState: -> @originalState = @serializeState() + createAutocomplete: -> + @searchInput.glDropdown + filterInputBlur: false + filterable: true + filterRemote: true + highlight: true + filterInput: 'input#search' + search: + fields: ['text'] + data: @getData.bind(@) + + getData: (term, callback) -> + _this = @ + + # Ensure this is not called when autocomplete is disabled because + # this method still will be called because `GitLabDropdownFilter` is triggering this on keyup + return if @autocomplete is false + + # Do not trigger request if input is empty + return if @searchInput.val() is '' + + # Prevent multiple ajax calls + return if @loadingSuggestions + + @loadingSuggestions = true + + jqXHR = $.get(@autocompletePath, { + project_id: @projectId + project_ref: @projectRef + term: term + }, (response) -> + data = [] + + # List results + for suggestion in response + + # Add group header before list each group + if lastCategory isnt suggestion.category + data.push + header: suggestion.category + + lastCategory = suggestion.category + + data.push + text: suggestion.label + url: suggestion.url + + callback(data) + ).always -> + _this.loadingSuggestions = false + serializeState: -> { # Search Criteria @@ -57,7 +110,8 @@ class @SearchAutocomplete } bindEvents: -> - @searchInput.on 'keydown', @onSearchInputKeyDown + @searchInput.on 'keyup', @onSearchInputKeyUp + @searchInput.on 'click', @onSearchInputClick @searchInput.on 'focus', @onSearchInputFocus @searchInput.on 'blur', @onSearchInputBlur @clearInput.on 'click', @onRemoveLocationClick @@ -67,53 +121,7 @@ class @SearchAutocomplete dropdownMenu = @dropdown.find('.dropdown-menu') _this = @ - loading = false - - @searchInput.glDropdown - filterInputBlur: false - filterable: true - filterRemote: true - highlight: true - filterInput: 'input#search' - search: - fields: ['text'] - data: (term, callback) -> - # Ensure this is not called when autocomplete is disabled because - # this method still will be called because `GitLabDropdownFilter` is triggering this on keyup - return if _this.autocomplete is false - - # Do not trigger request if input is empty - return if _this.searchInput.val() is '' - - # Prevent multiple ajax calls - return if loading - - loading = true - - jqXHR = $.get(_this.autocompletePath, { - project_id: _this.projectId - project_ref: _this.projectRef - term: term - }, (response) -> - data = [] - - # List results - for suggestion in response - - # Add group header before list each group - if lastCategory isnt suggestion.category - data.push - header: suggestion.category - - lastCategory = suggestion.category - - data.push - text: suggestion.label - url: suggestion.url - - callback(data) - ).always -> - loading = false + @loadingSuggestions = false @dropdown.addClass('open') @searchInput.removeClass('disabled') @@ -122,7 +130,7 @@ class @SearchAutocomplete onDropdownOpen: (e) => @dropdown.dropdown('toggle') - onSearchInputKeyDown: (e) => + onSearchInputKeyUp: (e) => switch e.keyCode when KEYCODE.BACKSPACE if e.currentTarget.value is '' @@ -139,11 +147,18 @@ class @SearchAutocomplete if @badgePresent() @disableAutocomplete() else - @enableAutocomplete() + + # We should display the menu only when input is not empty + if @searchInput.val() isnt '' + @enableAutocomplete() # Avoid falsy value to be returned return + onSearchInputClick: => + if (@searchInput.val() is '') + @disableAutocomplete() + onSearchInputFocus: => @wrap.addClass('search-active') diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 0a5c145029b..a7783365814 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -15,7 +15,10 @@ .dropdown{ data: {url: search_autocomplete_path } } = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } .dropdown-menu.dropdown-select - = dropdown_content + = dropdown_content do + %li + %a.is-focused + Loading... = dropdown_loading %i.search-icon %i.clear-icon.js-clear-input From c37735841b096eec5935e5c5ccb6d3d8b4f8234a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 23 Mar 2016 14:24:46 -0500 Subject: [PATCH 069/264] Restore menu content when emptying the search input --- app/assets/javascripts/search_autocomplete.js.coffee | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index a06c80b60cd..4aa658735de 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -18,6 +18,7 @@ class @SearchAutocomplete # Dropdown Element @dropdown = @wrap.find('.dropdown') + @dropdownContent = @dropdown.find('.dropdown-content') @locationBadgeEl = @getElement('.search-location-badge') @locationText = @getElement('.location-text') @@ -136,6 +137,7 @@ class @SearchAutocomplete if e.currentTarget.value is '' @removeLocationBadge() @searchInput.focus() + @disableAutocomplete() when KEYCODE.ESCAPE if @badgePresent() else @@ -239,4 +241,13 @@ class @SearchAutocomplete disableAutocomplete: -> if @autocomplete @searchInput.addClass('disabled') + @dropdown.removeClass('open') + @restoreMenu() + @autocomplete = false + + restoreMenu: -> + html = "" + @dropdownContent.html(html) From 30eeb453bd4ed0d710ae74a8bf5b8c8a48a2f96b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 23 Mar 2016 16:13:46 -0500 Subject: [PATCH 070/264] Remove unused instance variable --- .../javascripts/search_autocomplete.js.coffee | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 4aa658735de..a7130c5796e 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -35,7 +35,6 @@ class @SearchAutocomplete @createAutocomplete() @searchInput.addClass('disabled') - @autocomplete = false @bindEvents() @@ -60,10 +59,6 @@ class @SearchAutocomplete getData: (term, callback) -> _this = @ - # Ensure this is not called when autocomplete is disabled because - # this method still will be called because `GitLabDropdownFilter` is triggering this on keyup - return if @autocomplete is false - # Do not trigger request if input is empty return if @searchInput.val() is '' @@ -118,15 +113,12 @@ class @SearchAutocomplete @clearInput.on 'click', @onRemoveLocationClick enableAutocomplete: -> - return if @autocomplete - dropdownMenu = @dropdown.find('.dropdown-menu') _this = @ @loadingSuggestions = false @dropdown.addClass('open') @searchInput.removeClass('disabled') - @autocomplete = true onDropdownOpen: (e) => @dropdown.dropdown('toggle') @@ -239,12 +231,9 @@ class @SearchAutocomplete @wrap.removeClass('has-location-badge') disableAutocomplete: -> - if @autocomplete - @searchInput.addClass('disabled') - @dropdown.removeClass('open') - @restoreMenu() - - @autocomplete = false + @searchInput.addClass('disabled') + @dropdown.removeClass('open') + @restoreMenu() restoreMenu: -> html = "