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/618] 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 a789d2c814..dbcb2d6a00 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/618] 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 dbcb2d6a00..ce2434330e 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/618] 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 c1c90903d5..84739fab82 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 aea08ecce3..a26d11459f 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 c1e18bb3cc..18ec48b57f 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 65829bec77..859758293e 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 2fa38a5d3d..ba4fb2f822 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/618] 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 1be86e3b82..0aefea7d8d 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 0000000000..490032dc78 --- /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 c180136526..df31b07910 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 494dad0b41..9102fd6d50 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 54af2c3063..c500289383 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 0000000000..dfe8bc010d --- /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/618] 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 490032dc78..c85fabbcd5 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 df31b07910..a6d5ab6523 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 6c08005812..18136509da 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 4c4033e3ae..f72bd22348 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 0cdcd923b3..76b4cea477 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 84234b15c6..3c3313c911 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 9102fd6d50..cbead1b8b7 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 c500289383..843c833b4f 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 dfe8bc010d..f1ecc060cf 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/618] 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 a6d5ab6523..3cedf1c7b1 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/618] 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 3cedf1c7b1..0c4876358b 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 843c833b4f..58a3cdf955 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/618] 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 0000000000..3dd110cfa0 --- /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 3c3313c911..90c9d4de59 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 58a3cdf955..a004908fb6 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/618] 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 0c4876358b..b867190086 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 cbead1b8b7..de16454739 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/618] 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 3dd110cfa0..0000000000 --- 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 90c9d4de59..bc660985ec 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/618] 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 4f03847775..e763ca5c78 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/618] 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 e763ca5c78..0b0620a71c 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/618] 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 0b0620a71c..1d100c054d 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/618] 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 1d100c054d..042ae1d04b 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/618] 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 042ae1d04b..62199e5be0 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/618] 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 62199e5be0..79696cc679 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/618] 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 79696cc679..0684e7852f 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 b867190086..e21a140b2a 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 76b4cea477..85a6f4b8b5 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 bc660985ec..ff32bca98d 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 a004908fb6..f051e7a186 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/618] 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 c85fabbcd5..0000000000 --- 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/618] 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 0684e7852f..c579657d4d 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 f108cedf4a46ee0f6114a5a8584a6080f4346e5b Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 17 Mar 2016 07:56:23 +0100 Subject: [PATCH 020/618] Add description of technical debt label in docs [ci skip] --- CONTRIBUTING.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7540fa1afc..a946c2cd88 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,7 @@ - [Issue tracker guidelines](#issue-tracker-guidelines) - [Issue weight](#issue-weight) - [Regression issues](#regression-issues) + - [Technical debt](#technical-debt) - [Merge requests](#merge-requests) - [Merge request guidelines](#merge-request-guidelines) - [Merge request description format](#merge-request-description-format) @@ -242,6 +243,25 @@ addressed. [8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127 [update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue +### Technical debt + +In order to track things that can be improved in GitLab codebase, we created a +*technical debt* label in [issue tracker of CE][ce-tracker]. + +This label should be added to issues that describe things that can be improved, +shortcuts that has been taken, code that needs refactoring, features that need +additional attention, and all other things that have been left behind due to +high velocity of development. + +Everyone can create an issue (though you may need to ask for adding a specific +label, if you do not have permissions to do it by yourself), additional labels +can be combined with *technical debt* label, to make it easier to schedule the +improvements for a release. + +Issues with *technical debt* label have a same priority like issues that +describe a new features that can be introduced in GitLab, and should be +scheduled for a release by appropriate person. + ## Merge requests We welcome merge requests with fixes and improvements to GitLab code, tests, From 78530162261accaa01be0dc90bf8cd06acb210a6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 01:26:48 -0500 Subject: [PATCH 021/618] Add cropper library --- app/assets/javascripts/lib/cropper.js | 2993 +++++++++++++++++++++++ app/assets/stylesheets/application.scss | 1 + app/assets/stylesheets/cropper.css | 379 +++ 3 files changed, 3373 insertions(+) create mode 100644 app/assets/javascripts/lib/cropper.js create mode 100644 app/assets/stylesheets/cropper.css diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js new file mode 100644 index 0000000000..805485904a --- /dev/null +++ b/app/assets/javascripts/lib/cropper.js @@ -0,0 +1,2993 @@ +/*! + * Cropper v2.3.0 + * https://github.com/fengyuanchen/cropper + * + * Copyright (c) 2014-2016 Fengyuan Chen and contributors + * Released under the MIT license + * + * Date: 2016-02-22T02:13:13.332Z + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node / CommonJS + factory(require('jquery')); + } else { + // Browser globals. + factory(jQuery); + } +})(function ($) { + + 'use strict'; + + // Globals + var $window = $(window); + var $document = $(document); + var location = window.location; + var navigator = window.navigator; + var ArrayBuffer = window.ArrayBuffer; + var Uint8Array = window.Uint8Array; + var DataView = window.DataView; + var btoa = window.btoa; + + // Constants + var NAMESPACE = 'cropper'; + + // Classes + var CLASS_MODAL = 'cropper-modal'; + var CLASS_HIDE = 'cropper-hide'; + var CLASS_HIDDEN = 'cropper-hidden'; + var CLASS_INVISIBLE = 'cropper-invisible'; + var CLASS_MOVE = 'cropper-move'; + var CLASS_CROP = 'cropper-crop'; + var CLASS_DISABLED = 'cropper-disabled'; + var CLASS_BG = 'cropper-bg'; + + // Events + var EVENT_MOUSE_DOWN = 'mousedown touchstart pointerdown MSPointerDown'; + var EVENT_MOUSE_MOVE = 'mousemove touchmove pointermove MSPointerMove'; + var EVENT_MOUSE_UP = 'mouseup touchend touchcancel pointerup pointercancel MSPointerUp MSPointerCancel'; + var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll'; + var EVENT_DBLCLICK = 'dblclick'; + var EVENT_LOAD = 'load.' + NAMESPACE; + var EVENT_ERROR = 'error.' + NAMESPACE; + var EVENT_RESIZE = 'resize.' + NAMESPACE; // Bind to window with namespace + var EVENT_BUILD = 'build.' + NAMESPACE; + var EVENT_BUILT = 'built.' + NAMESPACE; + var EVENT_CROP_START = 'cropstart.' + NAMESPACE; + var EVENT_CROP_MOVE = 'cropmove.' + NAMESPACE; + var EVENT_CROP_END = 'cropend.' + NAMESPACE; + var EVENT_CROP = 'crop.' + NAMESPACE; + var EVENT_ZOOM = 'zoom.' + NAMESPACE; + + // RegExps + var REGEXP_ACTIONS = /e|w|s|n|se|sw|ne|nw|all|crop|move|zoom/; + var REGEXP_DATA_URL = /^data\:/; + var REGEXP_DATA_URL_HEAD = /^data\:([^\;]+)\;base64,/; + var REGEXP_DATA_URL_JPEG = /^data\:image\/jpeg.*;base64,/; + + // Data keys + var DATA_PREVIEW = 'preview'; + var DATA_ACTION = 'action'; + + // Actions + var ACTION_EAST = 'e'; + var ACTION_WEST = 'w'; + var ACTION_SOUTH = 's'; + var ACTION_NORTH = 'n'; + var ACTION_SOUTH_EAST = 'se'; + var ACTION_SOUTH_WEST = 'sw'; + var ACTION_NORTH_EAST = 'ne'; + var ACTION_NORTH_WEST = 'nw'; + var ACTION_ALL = 'all'; + var ACTION_CROP = 'crop'; + var ACTION_MOVE = 'move'; + var ACTION_ZOOM = 'zoom'; + var ACTION_NONE = 'none'; + + // Supports + var SUPPORT_CANVAS = $.isFunction($('')[0].getContext); + var IS_SAFARI = navigator && /safari/i.test(navigator.userAgent) && /apple computer/i.test(navigator.vendor); + + // Maths + var num = Number; + var min = Math.min; + var max = Math.max; + var abs = Math.abs; + var sin = Math.sin; + var cos = Math.cos; + var sqrt = Math.sqrt; + var round = Math.round; + var floor = Math.floor; + + // Utilities + var fromCharCode = String.fromCharCode; + + function isNumber(n) { + return typeof n === 'number' && !isNaN(n); + } + + function isUndefined(n) { + return typeof n === 'undefined'; + } + + function toArray(obj, offset) { + var args = []; + + // This is necessary for IE8 + if (isNumber(offset)) { + args.push(offset); + } + + return args.slice.apply(obj, args); + } + + // Custom proxy to avoid jQuery's guid + function proxy(fn, context) { + var args = toArray(arguments, 2); + + return function () { + return fn.apply(context, args.concat(toArray(arguments))); + }; + } + + function isCrossOriginURL(url) { + var parts = url.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i); + + return parts && ( + parts[1] !== location.protocol || + parts[2] !== location.hostname || + parts[3] !== location.port + ); + } + + function addTimestamp(url) { + var timestamp = 'timestamp=' + (new Date()).getTime(); + + return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp); + } + + function getCrossOrigin(crossOrigin) { + return crossOrigin ? ' crossOrigin="' + crossOrigin + '"' : ''; + } + + function getImageSize(image, callback) { + var newImage; + + // Modern browsers (ignore Safari, #120 & #509) + if (image.naturalWidth && !IS_SAFARI) { + return callback(image.naturalWidth, image.naturalHeight); + } + + // IE8: Don't use `new Image()` here (#319) + newImage = document.createElement('img'); + + newImage.onload = function () { + callback(this.width, this.height); + }; + + newImage.src = image.src; + } + + function getTransform(options) { + var transforms = []; + var rotate = options.rotate; + var scaleX = options.scaleX; + var scaleY = options.scaleY; + + if (isNumber(rotate)) { + transforms.push('rotate(' + rotate + 'deg)'); + } + + if (isNumber(scaleX) && isNumber(scaleY)) { + transforms.push('scale(' + scaleX + ',' + scaleY + ')'); + } + + return transforms.length ? transforms.join(' ') : 'none'; + } + + function getRotatedSizes(data, isReversed) { + var deg = abs(data.degree) % 180; + var arc = (deg > 90 ? (180 - deg) : deg) * Math.PI / 180; + var sinArc = sin(arc); + var cosArc = cos(arc); + var width = data.width; + var height = data.height; + var aspectRatio = data.aspectRatio; + var newWidth; + var newHeight; + + if (!isReversed) { + newWidth = width * cosArc + height * sinArc; + newHeight = width * sinArc + height * cosArc; + } else { + newWidth = width / (cosArc + sinArc / aspectRatio); + newHeight = newWidth / aspectRatio; + } + + return { + width: newWidth, + height: newHeight + }; + } + + function getSourceCanvas(image, data) { + var canvas = $('')[0]; + var context = canvas.getContext('2d'); + var dstX = 0; + var dstY = 0; + var dstWidth = data.naturalWidth; + var dstHeight = data.naturalHeight; + var rotate = data.rotate; + var scaleX = data.scaleX; + var scaleY = data.scaleY; + var scalable = isNumber(scaleX) && isNumber(scaleY) && (scaleX !== 1 || scaleY !== 1); + var rotatable = isNumber(rotate) && rotate !== 0; + var advanced = rotatable || scalable; + var canvasWidth = dstWidth * abs(scaleX || 1); + var canvasHeight = dstHeight * abs(scaleY || 1); + var translateX; + var translateY; + var rotated; + + if (scalable) { + translateX = canvasWidth / 2; + translateY = canvasHeight / 2; + } + + if (rotatable) { + rotated = getRotatedSizes({ + width: canvasWidth, + height: canvasHeight, + degree: rotate + }); + + canvasWidth = rotated.width; + canvasHeight = rotated.height; + translateX = canvasWidth / 2; + translateY = canvasHeight / 2; + } + + canvas.width = canvasWidth; + canvas.height = canvasHeight; + + if (advanced) { + dstX = -dstWidth / 2; + dstY = -dstHeight / 2; + + context.save(); + context.translate(translateX, translateY); + } + + if (rotatable) { + context.rotate(rotate * Math.PI / 180); + } + + // Should call `scale` after rotated + if (scalable) { + context.scale(scaleX, scaleY); + } + + context.drawImage(image, floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight)); + + if (advanced) { + context.restore(); + } + + return canvas; + } + + function getTouchesCenter(touches) { + var length = touches.length; + var pageX = 0; + var pageY = 0; + + if (length) { + $.each(touches, function (i, touch) { + pageX += touch.pageX; + pageY += touch.pageY; + }); + + pageX /= length; + pageY /= length; + } + + return { + pageX: pageX, + pageY: pageY + }; + } + + function getStringFromCharCode(dataView, start, length) { + var str = ''; + var i; + + for (i = start, length += start; i < length; i++) { + str += fromCharCode(dataView.getUint8(i)); + } + + return str; + } + + function getOrientation(arrayBuffer) { + var dataView = new DataView(arrayBuffer); + var length = dataView.byteLength; + var orientation; + var exifIDCode; + var tiffOffset; + var firstIFDOffset; + var littleEndian; + var endianness; + var app1Start; + var ifdStart; + var offset; + var i; + + // Only handle JPEG image (start by 0xFFD8) + if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) { + offset = 2; + + while (offset < length) { + if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) { + app1Start = offset; + break; + } + + offset++; + } + } + + if (app1Start) { + exifIDCode = app1Start + 4; + tiffOffset = app1Start + 10; + + if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') { + endianness = dataView.getUint16(tiffOffset); + littleEndian = endianness === 0x4949; + + if (littleEndian || endianness === 0x4D4D /* bigEndian */) { + if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) { + firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian); + + if (firstIFDOffset >= 0x00000008) { + ifdStart = tiffOffset + firstIFDOffset; + } + } + } + } + } + + if (ifdStart) { + length = dataView.getUint16(ifdStart, littleEndian); + + for (i = 0; i < length; i++) { + offset = ifdStart + i * 12 + 2; + + if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) { + + // 8 is the offset of the current tag's value + offset += 8; + + // Get the original orientation value + orientation = dataView.getUint16(offset, littleEndian); + + // Override the orientation with its default value for Safari (#120) + if (IS_SAFARI) { + dataView.setUint16(offset, 1, littleEndian); + } + + break; + } + } + } + + return orientation; + } + + function dataURLToArrayBuffer(dataURL) { + var base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, ''); + var binary = atob(base64); + var length = binary.length; + var arrayBuffer = new ArrayBuffer(length); + var dataView = new Uint8Array(arrayBuffer); + var i; + + for (i = 0; i < length; i++) { + dataView[i] = binary.charCodeAt(i); + } + + return arrayBuffer; + } + + // Only available for JPEG image + function arrayBufferToDataURL(arrayBuffer) { + var dataView = new Uint8Array(arrayBuffer); + var length = dataView.length; + var base64 = ''; + var i; + + for (i = 0; i < length; i++) { + base64 += fromCharCode(dataView[i]); + } + + return 'data:image/jpeg;base64,' + btoa(base64); + } + + function Cropper(element, options) { + this.$element = $(element); + this.options = $.extend({}, Cropper.DEFAULTS, $.isPlainObject(options) && options); + this.isLoaded = false; + this.isBuilt = false; + this.isCompleted = false; + this.isRotated = false; + this.isCropped = false; + this.isDisabled = false; + this.isReplaced = false; + this.isLimited = false; + this.wheeling = false; + this.isImg = false; + this.originalUrl = ''; + this.canvas = null; + this.cropBox = null; + this.init(); + } + + Cropper.prototype = { + constructor: Cropper, + + init: function () { + var $this = this.$element; + var url; + + if ($this.is('img')) { + this.isImg = true; + + // Should use `$.fn.attr` here. e.g.: "img/picture.jpg" + this.originalUrl = url = $this.attr('src'); + + // Stop when it's a blank image + if (!url) { + return; + } + + // Should use `$.fn.prop` here. e.g.: "http://example.com/img/picture.jpg" + url = $this.prop('src'); + } else if ($this.is('canvas') && SUPPORT_CANVAS) { + url = $this[0].toDataURL(); + } + + this.load(url); + }, + + // A shortcut for triggering custom events + trigger: function (type, data) { + var e = $.Event(type, data); + + this.$element.trigger(e); + + return e; + }, + + load: function (url) { + var options = this.options; + var $this = this.$element; + var read; + var xhr; + + if (!url) { + return; + } + + // Trigger build event first + $this.one(EVENT_BUILD, options.build); + + if (this.trigger(EVENT_BUILD).isDefaultPrevented()) { + return; + } + + this.url = url; + this.image = {}; + + if (!options.checkOrientation || !ArrayBuffer) { + return this.clone(); + } + + read = $.proxy(this.read, this); + + // XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari + if (REGEXP_DATA_URL.test(url)) { + return REGEXP_DATA_URL_JPEG.test(url) ? + read(dataURLToArrayBuffer(url)) : + this.clone(); + } + + xhr = new XMLHttpRequest(); + + xhr.onerror = xhr.onabort = $.proxy(function () { + this.clone(); + }, this); + + xhr.onload = function () { + read(this.response); + }; + + xhr.open('get', url); + xhr.responseType = 'arraybuffer'; + xhr.send(); + }, + + read: function (arrayBuffer) { + var options = this.options; + var orientation = getOrientation(arrayBuffer); + var image = this.image; + var rotate; + var scaleX; + var scaleY; + + if (orientation > 1) { + this.url = arrayBufferToDataURL(arrayBuffer); + + switch (orientation) { + + // flip horizontal + case 2: + scaleX = -1; + break; + + // rotate left 180° + case 3: + rotate = -180; + break; + + // flip vertical + case 4: + scaleY = -1; + break; + + // flip vertical + rotate right 90° + case 5: + rotate = 90; + scaleY = -1; + break; + + // rotate right 90° + case 6: + rotate = 90; + break; + + // flip horizontal + rotate right 90° + case 7: + rotate = 90; + scaleX = -1; + break; + + // rotate left 90° + case 8: + rotate = -90; + break; + } + } + + if (options.rotatable) { + image.rotate = rotate; + } + + if (options.scalable) { + image.scaleX = scaleX; + image.scaleY = scaleY; + } + + this.clone(); + }, + + clone: function () { + var options = this.options; + var $this = this.$element; + var url = this.url; + var crossOrigin = ''; + var crossOriginUrl; + var $clone; + + if (options.checkCrossOrigin && isCrossOriginURL(url)) { + crossOrigin = $this.prop('crossOrigin'); + + if (crossOrigin) { + crossOriginUrl = url; + } else { + crossOrigin = 'anonymous'; + + // Bust cache (#148) when there is not a "crossOrigin" property + crossOriginUrl = addTimestamp(url); + } + } + + this.crossOrigin = crossOrigin; + this.crossOriginUrl = crossOriginUrl; + this.$clone = $clone = $(''); + + if (this.isImg) { + if ($this[0].complete) { + this.start(); + } else { + $this.one(EVENT_LOAD, $.proxy(this.start, this)); + } + } else { + $clone. + one(EVENT_LOAD, $.proxy(this.start, this)). + one(EVENT_ERROR, $.proxy(this.stop, this)). + addClass(CLASS_HIDE). + insertAfter($this); + } + }, + + start: function () { + var $image = this.$element; + var $clone = this.$clone; + + if (!this.isImg) { + $clone.off(EVENT_ERROR, this.stop); + $image = $clone; + } + + getImageSize($image[0], $.proxy(function (naturalWidth, naturalHeight) { + $.extend(this.image, { + naturalWidth: naturalWidth, + naturalHeight: naturalHeight, + aspectRatio: naturalWidth / naturalHeight + }); + + this.isLoaded = true; + this.build(); + }, this)); + }, + + stop: function () { + this.$clone.remove(); + this.$clone = null; + }, + + build: function () { + var options = this.options; + var $this = this.$element; + var $clone = this.$clone; + var $cropper; + var $cropBox; + var $face; + + if (!this.isLoaded) { + return; + } + + // Unbuild first when replace + if (this.isBuilt) { + this.unbuild(); + } + + // Create cropper elements + this.$container = $this.parent(); + this.$cropper = $cropper = $(Cropper.TEMPLATE); + this.$canvas = $cropper.find('.cropper-canvas').append($clone); + this.$dragBox = $cropper.find('.cropper-drag-box'); + this.$cropBox = $cropBox = $cropper.find('.cropper-crop-box'); + this.$viewBox = $cropper.find('.cropper-view-box'); + this.$face = $face = $cropBox.find('.cropper-face'); + + // Hide the original image + $this.addClass(CLASS_HIDDEN).after($cropper); + + // Show the clone image if is hidden + if (!this.isImg) { + $clone.removeClass(CLASS_HIDE); + } + + this.initPreview(); + this.bind(); + + options.aspectRatio = max(0, options.aspectRatio) || NaN; + options.viewMode = max(0, min(3, round(options.viewMode))) || 0; + + if (options.autoCrop) { + this.isCropped = true; + + if (options.modal) { + this.$dragBox.addClass(CLASS_MODAL); + } + } else { + $cropBox.addClass(CLASS_HIDDEN); + } + + if (!options.guides) { + $cropBox.find('.cropper-dashed').addClass(CLASS_HIDDEN); + } + + if (!options.center) { + $cropBox.find('.cropper-center').addClass(CLASS_HIDDEN); + } + + if (options.cropBoxMovable) { + $face.addClass(CLASS_MOVE).data(DATA_ACTION, ACTION_ALL); + } + + if (!options.highlight) { + $face.addClass(CLASS_INVISIBLE); + } + + if (options.background) { + $cropper.addClass(CLASS_BG); + } + + if (!options.cropBoxResizable) { + $cropBox.find('.cropper-line, .cropper-point').addClass(CLASS_HIDDEN); + } + + this.setDragMode(options.dragMode); + this.render(); + this.isBuilt = true; + this.setData(options.data); + $this.one(EVENT_BUILT, options.built); + + // Trigger the built event asynchronously to keep `data('cropper')` is defined + setTimeout($.proxy(function () { + this.trigger(EVENT_BUILT); + this.isCompleted = true; + }, this), 0); + }, + + unbuild: function () { + if (!this.isBuilt) { + return; + } + + this.isBuilt = false; + this.isCompleted = false; + this.initialImage = null; + + // Clear `initialCanvas` is necessary when replace + this.initialCanvas = null; + this.initialCropBox = null; + this.container = null; + this.canvas = null; + + // Clear `cropBox` is necessary when replace + this.cropBox = null; + this.unbind(); + + this.resetPreview(); + this.$preview = null; + + this.$viewBox = null; + this.$cropBox = null; + this.$dragBox = null; + this.$canvas = null; + this.$container = null; + + this.$cropper.remove(); + this.$cropper = null; + }, + + render: function () { + this.initContainer(); + this.initCanvas(); + this.initCropBox(); + + this.renderCanvas(); + + if (this.isCropped) { + this.renderCropBox(); + } + }, + + initContainer: function () { + var options = this.options; + var $this = this.$element; + var $container = this.$container; + var $cropper = this.$cropper; + + $cropper.addClass(CLASS_HIDDEN); + $this.removeClass(CLASS_HIDDEN); + + $cropper.css((this.container = { + width: max($container.width(), num(options.minContainerWidth) || 200), + height: max($container.height(), num(options.minContainerHeight) || 100) + })); + + $this.addClass(CLASS_HIDDEN); + $cropper.removeClass(CLASS_HIDDEN); + }, + + // Canvas (image wrapper) + initCanvas: function () { + var viewMode = this.options.viewMode; + var container = this.container; + var containerWidth = container.width; + var containerHeight = container.height; + var image = this.image; + var imageNaturalWidth = image.naturalWidth; + var imageNaturalHeight = image.naturalHeight; + var is90Degree = abs(image.rotate) === 90; + var naturalWidth = is90Degree ? imageNaturalHeight : imageNaturalWidth; + var naturalHeight = is90Degree ? imageNaturalWidth : imageNaturalHeight; + var aspectRatio = naturalWidth / naturalHeight; + var canvasWidth = containerWidth; + var canvasHeight = containerHeight; + var canvas; + + if (containerHeight * aspectRatio > containerWidth) { + if (viewMode === 3) { + canvasWidth = containerHeight * aspectRatio; + } else { + canvasHeight = containerWidth / aspectRatio; + } + } else { + if (viewMode === 3) { + canvasHeight = containerWidth / aspectRatio; + } else { + canvasWidth = containerHeight * aspectRatio; + } + } + + canvas = { + naturalWidth: naturalWidth, + naturalHeight: naturalHeight, + aspectRatio: aspectRatio, + width: canvasWidth, + height: canvasHeight + }; + + canvas.oldLeft = canvas.left = (containerWidth - canvasWidth) / 2; + canvas.oldTop = canvas.top = (containerHeight - canvasHeight) / 2; + + this.canvas = canvas; + this.isLimited = (viewMode === 1 || viewMode === 2); + this.limitCanvas(true, true); + this.initialImage = $.extend({}, image); + this.initialCanvas = $.extend({}, canvas); + }, + + limitCanvas: function (isSizeLimited, isPositionLimited) { + var options = this.options; + var viewMode = options.viewMode; + var container = this.container; + var containerWidth = container.width; + var containerHeight = container.height; + var canvas = this.canvas; + var aspectRatio = canvas.aspectRatio; + var cropBox = this.cropBox; + var isCropped = this.isCropped && cropBox; + var minCanvasWidth; + var minCanvasHeight; + var newCanvasLeft; + var newCanvasTop; + + if (isSizeLimited) { + minCanvasWidth = num(options.minCanvasWidth) || 0; + minCanvasHeight = num(options.minCanvasHeight) || 0; + + if (viewMode) { + if (viewMode > 1) { + minCanvasWidth = max(minCanvasWidth, containerWidth); + minCanvasHeight = max(minCanvasHeight, containerHeight); + + if (viewMode === 3) { + if (minCanvasHeight * aspectRatio > minCanvasWidth) { + minCanvasWidth = minCanvasHeight * aspectRatio; + } else { + minCanvasHeight = minCanvasWidth / aspectRatio; + } + } + } else { + if (minCanvasWidth) { + minCanvasWidth = max(minCanvasWidth, isCropped ? cropBox.width : 0); + } else if (minCanvasHeight) { + minCanvasHeight = max(minCanvasHeight, isCropped ? cropBox.height : 0); + } else if (isCropped) { + minCanvasWidth = cropBox.width; + minCanvasHeight = cropBox.height; + + if (minCanvasHeight * aspectRatio > minCanvasWidth) { + minCanvasWidth = minCanvasHeight * aspectRatio; + } else { + minCanvasHeight = minCanvasWidth / aspectRatio; + } + } + } + } + + if (minCanvasWidth && minCanvasHeight) { + if (minCanvasHeight * aspectRatio > minCanvasWidth) { + minCanvasHeight = minCanvasWidth / aspectRatio; + } else { + minCanvasWidth = minCanvasHeight * aspectRatio; + } + } else if (minCanvasWidth) { + minCanvasHeight = minCanvasWidth / aspectRatio; + } else if (minCanvasHeight) { + minCanvasWidth = minCanvasHeight * aspectRatio; + } + + canvas.minWidth = minCanvasWidth; + canvas.minHeight = minCanvasHeight; + canvas.maxWidth = Infinity; + canvas.maxHeight = Infinity; + } + + if (isPositionLimited) { + if (viewMode) { + newCanvasLeft = containerWidth - canvas.width; + newCanvasTop = containerHeight - canvas.height; + + canvas.minLeft = min(0, newCanvasLeft); + canvas.minTop = min(0, newCanvasTop); + canvas.maxLeft = max(0, newCanvasLeft); + canvas.maxTop = max(0, newCanvasTop); + + if (isCropped && this.isLimited) { + canvas.minLeft = min( + cropBox.left, + cropBox.left + cropBox.width - canvas.width + ); + canvas.minTop = min( + cropBox.top, + cropBox.top + cropBox.height - canvas.height + ); + canvas.maxLeft = cropBox.left; + canvas.maxTop = cropBox.top; + + if (viewMode === 2) { + if (canvas.width >= containerWidth) { + canvas.minLeft = min(0, newCanvasLeft); + canvas.maxLeft = max(0, newCanvasLeft); + } + + if (canvas.height >= containerHeight) { + canvas.minTop = min(0, newCanvasTop); + canvas.maxTop = max(0, newCanvasTop); + } + } + } + } else { + canvas.minLeft = -canvas.width; + canvas.minTop = -canvas.height; + canvas.maxLeft = containerWidth; + canvas.maxTop = containerHeight; + } + } + }, + + renderCanvas: function (isChanged) { + var canvas = this.canvas; + var image = this.image; + var rotate = image.rotate; + var naturalWidth = image.naturalWidth; + var naturalHeight = image.naturalHeight; + var aspectRatio; + var rotated; + + if (this.isRotated) { + this.isRotated = false; + + // Computes rotated sizes with image sizes + rotated = getRotatedSizes({ + width: image.width, + height: image.height, + degree: rotate + }); + + aspectRatio = rotated.width / rotated.height; + + if (aspectRatio !== canvas.aspectRatio) { + canvas.left -= (rotated.width - canvas.width) / 2; + canvas.top -= (rotated.height - canvas.height) / 2; + canvas.width = rotated.width; + canvas.height = rotated.height; + canvas.aspectRatio = aspectRatio; + canvas.naturalWidth = naturalWidth; + canvas.naturalHeight = naturalHeight; + + // Computes rotated sizes with natural image sizes + if (rotate % 180) { + rotated = getRotatedSizes({ + width: naturalWidth, + height: naturalHeight, + degree: rotate + }); + + canvas.naturalWidth = rotated.width; + canvas.naturalHeight = rotated.height; + } + + this.limitCanvas(true, false); + } + } + + if (canvas.width > canvas.maxWidth || canvas.width < canvas.minWidth) { + canvas.left = canvas.oldLeft; + } + + if (canvas.height > canvas.maxHeight || canvas.height < canvas.minHeight) { + canvas.top = canvas.oldTop; + } + + canvas.width = min(max(canvas.width, canvas.minWidth), canvas.maxWidth); + canvas.height = min(max(canvas.height, canvas.minHeight), canvas.maxHeight); + + this.limitCanvas(false, true); + + canvas.oldLeft = canvas.left = min(max(canvas.left, canvas.minLeft), canvas.maxLeft); + canvas.oldTop = canvas.top = min(max(canvas.top, canvas.minTop), canvas.maxTop); + + this.$canvas.css({ + width: canvas.width, + height: canvas.height, + left: canvas.left, + top: canvas.top + }); + + this.renderImage(); + + if (this.isCropped && this.isLimited) { + this.limitCropBox(true, true); + } + + if (isChanged) { + this.output(); + } + }, + + renderImage: function (isChanged) { + var canvas = this.canvas; + var image = this.image; + var reversed; + + if (image.rotate) { + reversed = getRotatedSizes({ + width: canvas.width, + height: canvas.height, + degree: image.rotate, + aspectRatio: image.aspectRatio + }, true); + } + + $.extend(image, reversed ? { + width: reversed.width, + height: reversed.height, + left: (canvas.width - reversed.width) / 2, + top: (canvas.height - reversed.height) / 2 + } : { + width: canvas.width, + height: canvas.height, + left: 0, + top: 0 + }); + + this.$clone.css({ + width: image.width, + height: image.height, + marginLeft: image.left, + marginTop: image.top, + transform: getTransform(image) + }); + + if (isChanged) { + this.output(); + } + }, + + initCropBox: function () { + var options = this.options; + var canvas = this.canvas; + var aspectRatio = options.aspectRatio; + var autoCropArea = num(options.autoCropArea) || 0.8; + var cropBox = { + width: canvas.width, + height: canvas.height + }; + + if (aspectRatio) { + if (canvas.height * aspectRatio > canvas.width) { + cropBox.height = cropBox.width / aspectRatio; + } else { + cropBox.width = cropBox.height * aspectRatio; + } + } + + this.cropBox = cropBox; + this.limitCropBox(true, true); + + // Initialize auto crop area + cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth); + cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight); + + // The width of auto crop area must large than "minWidth", and the height too. (#164) + cropBox.width = max(cropBox.minWidth, cropBox.width * autoCropArea); + cropBox.height = max(cropBox.minHeight, cropBox.height * autoCropArea); + cropBox.oldLeft = cropBox.left = canvas.left + (canvas.width - cropBox.width) / 2; + cropBox.oldTop = cropBox.top = canvas.top + (canvas.height - cropBox.height) / 2; + + this.initialCropBox = $.extend({}, cropBox); + }, + + limitCropBox: function (isSizeLimited, isPositionLimited) { + var options = this.options; + var aspectRatio = options.aspectRatio; + var container = this.container; + var containerWidth = container.width; + var containerHeight = container.height; + var canvas = this.canvas; + var cropBox = this.cropBox; + var isLimited = this.isLimited; + var minCropBoxWidth; + var minCropBoxHeight; + var maxCropBoxWidth; + var maxCropBoxHeight; + + if (isSizeLimited) { + minCropBoxWidth = num(options.minCropBoxWidth) || 0; + minCropBoxHeight = num(options.minCropBoxHeight) || 0; + + // The min/maxCropBoxWidth/Height must be less than containerWidth/Height + minCropBoxWidth = min(minCropBoxWidth, containerWidth); + minCropBoxHeight = min(minCropBoxHeight, containerHeight); + maxCropBoxWidth = min(containerWidth, isLimited ? canvas.width : containerWidth); + maxCropBoxHeight = min(containerHeight, isLimited ? canvas.height : containerHeight); + + if (aspectRatio) { + if (minCropBoxWidth && minCropBoxHeight) { + if (minCropBoxHeight * aspectRatio > minCropBoxWidth) { + minCropBoxHeight = minCropBoxWidth / aspectRatio; + } else { + minCropBoxWidth = minCropBoxHeight * aspectRatio; + } + } else if (minCropBoxWidth) { + minCropBoxHeight = minCropBoxWidth / aspectRatio; + } else if (minCropBoxHeight) { + minCropBoxWidth = minCropBoxHeight * aspectRatio; + } + + if (maxCropBoxHeight * aspectRatio > maxCropBoxWidth) { + maxCropBoxHeight = maxCropBoxWidth / aspectRatio; + } else { + maxCropBoxWidth = maxCropBoxHeight * aspectRatio; + } + } + + // The minWidth/Height must be less than maxWidth/Height + cropBox.minWidth = min(minCropBoxWidth, maxCropBoxWidth); + cropBox.minHeight = min(minCropBoxHeight, maxCropBoxHeight); + cropBox.maxWidth = maxCropBoxWidth; + cropBox.maxHeight = maxCropBoxHeight; + } + + if (isPositionLimited) { + if (isLimited) { + cropBox.minLeft = max(0, canvas.left); + cropBox.minTop = max(0, canvas.top); + cropBox.maxLeft = min(containerWidth, canvas.left + canvas.width) - cropBox.width; + cropBox.maxTop = min(containerHeight, canvas.top + canvas.height) - cropBox.height; + } else { + cropBox.minLeft = 0; + cropBox.minTop = 0; + cropBox.maxLeft = containerWidth - cropBox.width; + cropBox.maxTop = containerHeight - cropBox.height; + } + } + }, + + renderCropBox: function () { + var options = this.options; + var container = this.container; + var containerWidth = container.width; + var containerHeight = container.height; + var cropBox = this.cropBox; + + if (cropBox.width > cropBox.maxWidth || cropBox.width < cropBox.minWidth) { + cropBox.left = cropBox.oldLeft; + } + + if (cropBox.height > cropBox.maxHeight || cropBox.height < cropBox.minHeight) { + cropBox.top = cropBox.oldTop; + } + + cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth); + cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight); + + this.limitCropBox(false, true); + + cropBox.oldLeft = cropBox.left = min(max(cropBox.left, cropBox.minLeft), cropBox.maxLeft); + cropBox.oldTop = cropBox.top = min(max(cropBox.top, cropBox.minTop), cropBox.maxTop); + + if (options.movable && options.cropBoxMovable) { + + // Turn to move the canvas when the crop box is equal to the container + this.$face.data(DATA_ACTION, (cropBox.width === containerWidth && cropBox.height === containerHeight) ? ACTION_MOVE : ACTION_ALL); + } + + this.$cropBox.css({ + width: cropBox.width, + height: cropBox.height, + left: cropBox.left, + top: cropBox.top + }); + + if (this.isCropped && this.isLimited) { + this.limitCanvas(true, true); + } + + if (!this.isDisabled) { + this.output(); + } + }, + + output: function () { + this.preview(); + + if (this.isCompleted) { + this.trigger(EVENT_CROP, this.getData()); + } else if (!this.isBuilt) { + + // Only trigger one crop event before complete + this.$element.one(EVENT_BUILT, $.proxy(function () { + this.trigger(EVENT_CROP, this.getData()); + }, this)); + } + }, + + initPreview: function () { + var crossOrigin = getCrossOrigin(this.crossOrigin); + var url = crossOrigin ? this.crossOriginUrl : this.url; + var $clone2; + + this.$preview = $(this.options.preview); + this.$clone2 = $clone2 = $(''); + this.$viewBox.html($clone2); + this.$preview.each(function () { + var $this = $(this); + + // Save the original size for recover + $this.data(DATA_PREVIEW, { + width: $this.width(), + height: $this.height(), + html: $this.html() + }); + + /** + * Override img element styles + * Add `display:block` to avoid margin top issue + * (Occur only when margin-top <= -height) + */ + $this.html( + '' + ); + }); + }, + + resetPreview: function () { + this.$preview.each(function () { + var $this = $(this); + var data = $this.data(DATA_PREVIEW); + + $this.css({ + width: data.width, + height: data.height + }).html(data.html).removeData(DATA_PREVIEW); + }); + }, + + preview: function () { + var image = this.image; + var canvas = this.canvas; + var cropBox = this.cropBox; + var cropBoxWidth = cropBox.width; + var cropBoxHeight = cropBox.height; + var width = image.width; + var height = image.height; + var left = cropBox.left - canvas.left - image.left; + var top = cropBox.top - canvas.top - image.top; + + if (!this.isCropped || this.isDisabled) { + return; + } + + this.$clone2.css({ + width: width, + height: height, + marginLeft: -left, + marginTop: -top, + transform: getTransform(image) + }); + + this.$preview.each(function () { + var $this = $(this); + var data = $this.data(DATA_PREVIEW); + var originalWidth = data.width; + var originalHeight = data.height; + var newWidth = originalWidth; + var newHeight = originalHeight; + var ratio = 1; + + if (cropBoxWidth) { + ratio = originalWidth / cropBoxWidth; + newHeight = cropBoxHeight * ratio; + } + + if (cropBoxHeight && newHeight > originalHeight) { + ratio = originalHeight / cropBoxHeight; + newWidth = cropBoxWidth * ratio; + newHeight = originalHeight; + } + + $this.css({ + width: newWidth, + height: newHeight + }).find('img').css({ + width: width * ratio, + height: height * ratio, + marginLeft: -left * ratio, + marginTop: -top * ratio, + transform: getTransform(image) + }); + }); + }, + + bind: function () { + var options = this.options; + var $this = this.$element; + var $cropper = this.$cropper; + + if ($.isFunction(options.cropstart)) { + $this.on(EVENT_CROP_START, options.cropstart); + } + + if ($.isFunction(options.cropmove)) { + $this.on(EVENT_CROP_MOVE, options.cropmove); + } + + if ($.isFunction(options.cropend)) { + $this.on(EVENT_CROP_END, options.cropend); + } + + if ($.isFunction(options.crop)) { + $this.on(EVENT_CROP, options.crop); + } + + if ($.isFunction(options.zoom)) { + $this.on(EVENT_ZOOM, options.zoom); + } + + $cropper.on(EVENT_MOUSE_DOWN, $.proxy(this.cropStart, this)); + + if (options.zoomable && options.zoomOnWheel) { + $cropper.on(EVENT_WHEEL, $.proxy(this.wheel, this)); + } + + if (options.toggleDragModeOnDblclick) { + $cropper.on(EVENT_DBLCLICK, $.proxy(this.dblclick, this)); + } + + $document. + on(EVENT_MOUSE_MOVE, (this._cropMove = proxy(this.cropMove, this))). + on(EVENT_MOUSE_UP, (this._cropEnd = proxy(this.cropEnd, this))); + + if (options.responsive) { + $window.on(EVENT_RESIZE, (this._resize = proxy(this.resize, this))); + } + }, + + unbind: function () { + var options = this.options; + var $this = this.$element; + var $cropper = this.$cropper; + + if ($.isFunction(options.cropstart)) { + $this.off(EVENT_CROP_START, options.cropstart); + } + + if ($.isFunction(options.cropmove)) { + $this.off(EVENT_CROP_MOVE, options.cropmove); + } + + if ($.isFunction(options.cropend)) { + $this.off(EVENT_CROP_END, options.cropend); + } + + if ($.isFunction(options.crop)) { + $this.off(EVENT_CROP, options.crop); + } + + if ($.isFunction(options.zoom)) { + $this.off(EVENT_ZOOM, options.zoom); + } + + $cropper.off(EVENT_MOUSE_DOWN, this.cropStart); + + if (options.zoomable && options.zoomOnWheel) { + $cropper.off(EVENT_WHEEL, this.wheel); + } + + if (options.toggleDragModeOnDblclick) { + $cropper.off(EVENT_DBLCLICK, this.dblclick); + } + + $document. + off(EVENT_MOUSE_MOVE, this._cropMove). + off(EVENT_MOUSE_UP, this._cropEnd); + + if (options.responsive) { + $window.off(EVENT_RESIZE, this._resize); + } + }, + + resize: function () { + var restore = this.options.restore; + var $container = this.$container; + var container = this.container; + var canvasData; + var cropBoxData; + var ratio; + + // Check `container` is necessary for IE8 + if (this.isDisabled || !container) { + return; + } + + ratio = $container.width() / container.width; + + // Resize when width changed or height changed + if (ratio !== 1 || $container.height() !== container.height) { + if (restore) { + canvasData = this.getCanvasData(); + cropBoxData = this.getCropBoxData(); + } + + this.render(); + + if (restore) { + this.setCanvasData($.each(canvasData, function (i, n) { + canvasData[i] = n * ratio; + })); + this.setCropBoxData($.each(cropBoxData, function (i, n) { + cropBoxData[i] = n * ratio; + })); + } + } + }, + + dblclick: function () { + if (this.isDisabled) { + return; + } + + if (this.$dragBox.hasClass(CLASS_CROP)) { + this.setDragMode(ACTION_MOVE); + } else { + this.setDragMode(ACTION_CROP); + } + }, + + wheel: function (event) { + var e = event.originalEvent || event; + var ratio = num(this.options.wheelZoomRatio) || 0.1; + var delta = 1; + + if (this.isDisabled) { + return; + } + + event.preventDefault(); + + // Limit wheel speed to prevent zoom too fast + if (this.wheeling) { + return; + } + + this.wheeling = true; + + setTimeout($.proxy(function () { + this.wheeling = false; + }, this), 50); + + if (e.deltaY) { + delta = e.deltaY > 0 ? 1 : -1; + } else if (e.wheelDelta) { + delta = -e.wheelDelta / 120; + } else if (e.detail) { + delta = e.detail > 0 ? 1 : -1; + } + + this.zoom(-delta * ratio, event); + }, + + cropStart: function (event) { + var options = this.options; + var originalEvent = event.originalEvent; + var touches = originalEvent && originalEvent.touches; + var e = event; + var touchesLength; + var action; + + if (this.isDisabled) { + return; + } + + if (touches) { + touchesLength = touches.length; + + if (touchesLength > 1) { + if (options.zoomable && options.zoomOnTouch && touchesLength === 2) { + e = touches[1]; + this.startX2 = e.pageX; + this.startY2 = e.pageY; + action = ACTION_ZOOM; + } else { + return; + } + } + + e = touches[0]; + } + + action = action || $(e.target).data(DATA_ACTION); + + if (REGEXP_ACTIONS.test(action)) { + if (this.trigger(EVENT_CROP_START, { + originalEvent: originalEvent, + action: action + }).isDefaultPrevented()) { + return; + } + + event.preventDefault(); + + this.action = action; + this.cropping = false; + + // IE8 has `event.pageX/Y`, but not `event.originalEvent.pageX/Y` + // IE10 has `event.originalEvent.pageX/Y`, but not `event.pageX/Y` + this.startX = e.pageX || originalEvent && originalEvent.pageX; + this.startY = e.pageY || originalEvent && originalEvent.pageY; + + if (action === ACTION_CROP) { + this.cropping = true; + this.$dragBox.addClass(CLASS_MODAL); + } + } + }, + + cropMove: function (event) { + var options = this.options; + var originalEvent = event.originalEvent; + var touches = originalEvent && originalEvent.touches; + var e = event; + var action = this.action; + var touchesLength; + + if (this.isDisabled) { + return; + } + + if (touches) { + touchesLength = touches.length; + + if (touchesLength > 1) { + if (options.zoomable && options.zoomOnTouch && touchesLength === 2) { + e = touches[1]; + this.endX2 = e.pageX; + this.endY2 = e.pageY; + } else { + return; + } + } + + e = touches[0]; + } + + if (action) { + if (this.trigger(EVENT_CROP_MOVE, { + originalEvent: originalEvent, + action: action + }).isDefaultPrevented()) { + return; + } + + event.preventDefault(); + + this.endX = e.pageX || originalEvent && originalEvent.pageX; + this.endY = e.pageY || originalEvent && originalEvent.pageY; + + this.change(e.shiftKey, action === ACTION_ZOOM ? event : null); + } + }, + + cropEnd: function (event) { + var originalEvent = event.originalEvent; + var action = this.action; + + if (this.isDisabled) { + return; + } + + if (action) { + event.preventDefault(); + + if (this.cropping) { + this.cropping = false; + this.$dragBox.toggleClass(CLASS_MODAL, this.isCropped && this.options.modal); + } + + this.action = ''; + + this.trigger(EVENT_CROP_END, { + originalEvent: originalEvent, + action: action + }); + } + }, + + change: function (shiftKey, event) { + var options = this.options; + var aspectRatio = options.aspectRatio; + var action = this.action; + var container = this.container; + var canvas = this.canvas; + var cropBox = this.cropBox; + var width = cropBox.width; + var height = cropBox.height; + var left = cropBox.left; + var top = cropBox.top; + var right = left + width; + var bottom = top + height; + var minLeft = 0; + var minTop = 0; + var maxWidth = container.width; + var maxHeight = container.height; + var renderable = true; + var offset; + var range; + + // Locking aspect ratio in "free mode" by holding shift key (#259) + if (!aspectRatio && shiftKey) { + aspectRatio = width && height ? width / height : 1; + } + + if (this.limited) { + minLeft = cropBox.minLeft; + minTop = cropBox.minTop; + maxWidth = minLeft + min(container.width, canvas.left + canvas.width); + maxHeight = minTop + min(container.height, canvas.top + canvas.height); + } + + range = { + x: this.endX - this.startX, + y: this.endY - this.startY + }; + + if (aspectRatio) { + range.X = range.y * aspectRatio; + range.Y = range.x / aspectRatio; + } + + switch (action) { + // Move crop box + case ACTION_ALL: + left += range.x; + top += range.y; + break; + + // Resize crop box + case ACTION_EAST: + if (range.x >= 0 && (right >= maxWidth || aspectRatio && + (top <= minTop || bottom >= maxHeight))) { + + renderable = false; + break; + } + + width += range.x; + + if (aspectRatio) { + height = width / aspectRatio; + top -= range.Y / 2; + } + + if (width < 0) { + action = ACTION_WEST; + width = 0; + } + + break; + + case ACTION_NORTH: + if (range.y <= 0 && (top <= minTop || aspectRatio && + (left <= minLeft || right >= maxWidth))) { + + renderable = false; + break; + } + + height -= range.y; + top += range.y; + + if (aspectRatio) { + width = height * aspectRatio; + left += range.X / 2; + } + + if (height < 0) { + action = ACTION_SOUTH; + height = 0; + } + + break; + + case ACTION_WEST: + if (range.x <= 0 && (left <= minLeft || aspectRatio && + (top <= minTop || bottom >= maxHeight))) { + + renderable = false; + break; + } + + width -= range.x; + left += range.x; + + if (aspectRatio) { + height = width / aspectRatio; + top += range.Y / 2; + } + + if (width < 0) { + action = ACTION_EAST; + width = 0; + } + + break; + + case ACTION_SOUTH: + if (range.y >= 0 && (bottom >= maxHeight || aspectRatio && + (left <= minLeft || right >= maxWidth))) { + + renderable = false; + break; + } + + height += range.y; + + if (aspectRatio) { + width = height * aspectRatio; + left -= range.X / 2; + } + + if (height < 0) { + action = ACTION_NORTH; + height = 0; + } + + break; + + case ACTION_NORTH_EAST: + if (aspectRatio) { + if (range.y <= 0 && (top <= minTop || right >= maxWidth)) { + renderable = false; + break; + } + + height -= range.y; + top += range.y; + width = height * aspectRatio; + } else { + if (range.x >= 0) { + if (right < maxWidth) { + width += range.x; + } else if (range.y <= 0 && top <= minTop) { + renderable = false; + } + } else { + width += range.x; + } + + if (range.y <= 0) { + if (top > minTop) { + height -= range.y; + top += range.y; + } + } else { + height -= range.y; + top += range.y; + } + } + + if (width < 0 && height < 0) { + action = ACTION_SOUTH_WEST; + height = 0; + width = 0; + } else if (width < 0) { + action = ACTION_NORTH_WEST; + width = 0; + } else if (height < 0) { + action = ACTION_SOUTH_EAST; + height = 0; + } + + break; + + case ACTION_NORTH_WEST: + if (aspectRatio) { + if (range.y <= 0 && (top <= minTop || left <= minLeft)) { + renderable = false; + break; + } + + height -= range.y; + top += range.y; + width = height * aspectRatio; + left += range.X; + } else { + if (range.x <= 0) { + if (left > minLeft) { + width -= range.x; + left += range.x; + } else if (range.y <= 0 && top <= minTop) { + renderable = false; + } + } else { + width -= range.x; + left += range.x; + } + + if (range.y <= 0) { + if (top > minTop) { + height -= range.y; + top += range.y; + } + } else { + height -= range.y; + top += range.y; + } + } + + if (width < 0 && height < 0) { + action = ACTION_SOUTH_EAST; + height = 0; + width = 0; + } else if (width < 0) { + action = ACTION_NORTH_EAST; + width = 0; + } else if (height < 0) { + action = ACTION_SOUTH_WEST; + height = 0; + } + + break; + + case ACTION_SOUTH_WEST: + if (aspectRatio) { + if (range.x <= 0 && (left <= minLeft || bottom >= maxHeight)) { + renderable = false; + break; + } + + width -= range.x; + left += range.x; + height = width / aspectRatio; + } else { + if (range.x <= 0) { + if (left > minLeft) { + width -= range.x; + left += range.x; + } else if (range.y >= 0 && bottom >= maxHeight) { + renderable = false; + } + } else { + width -= range.x; + left += range.x; + } + + if (range.y >= 0) { + if (bottom < maxHeight) { + height += range.y; + } + } else { + height += range.y; + } + } + + if (width < 0 && height < 0) { + action = ACTION_NORTH_EAST; + height = 0; + width = 0; + } else if (width < 0) { + action = ACTION_SOUTH_EAST; + width = 0; + } else if (height < 0) { + action = ACTION_NORTH_WEST; + height = 0; + } + + break; + + case ACTION_SOUTH_EAST: + if (aspectRatio) { + if (range.x >= 0 && (right >= maxWidth || bottom >= maxHeight)) { + renderable = false; + break; + } + + width += range.x; + height = width / aspectRatio; + } else { + if (range.x >= 0) { + if (right < maxWidth) { + width += range.x; + } else if (range.y >= 0 && bottom >= maxHeight) { + renderable = false; + } + } else { + width += range.x; + } + + if (range.y >= 0) { + if (bottom < maxHeight) { + height += range.y; + } + } else { + height += range.y; + } + } + + if (width < 0 && height < 0) { + action = ACTION_NORTH_WEST; + height = 0; + width = 0; + } else if (width < 0) { + action = ACTION_SOUTH_WEST; + width = 0; + } else if (height < 0) { + action = ACTION_NORTH_EAST; + height = 0; + } + + break; + + // Move canvas + case ACTION_MOVE: + this.move(range.x, range.y); + renderable = false; + break; + + // Zoom canvas + case ACTION_ZOOM: + this.zoom((function (x1, y1, x2, y2) { + var z1 = sqrt(x1 * x1 + y1 * y1); + var z2 = sqrt(x2 * x2 + y2 * y2); + + return (z2 - z1) / z1; + })( + abs(this.startX - this.startX2), + abs(this.startY - this.startY2), + abs(this.endX - this.endX2), + abs(this.endY - this.endY2) + ), event); + this.startX2 = this.endX2; + this.startY2 = this.endY2; + renderable = false; + break; + + // Create crop box + case ACTION_CROP: + if (!range.x || !range.y) { + renderable = false; + break; + } + + offset = this.$cropper.offset(); + left = this.startX - offset.left; + top = this.startY - offset.top; + width = cropBox.minWidth; + height = cropBox.minHeight; + + if (range.x > 0) { + action = range.y > 0 ? ACTION_SOUTH_EAST : ACTION_NORTH_EAST; + } else if (range.x < 0) { + left -= width; + action = range.y > 0 ? ACTION_SOUTH_WEST : ACTION_NORTH_WEST; + } + + if (range.y < 0) { + top -= height; + } + + // Show the crop box if is hidden + if (!this.isCropped) { + this.$cropBox.removeClass(CLASS_HIDDEN); + this.isCropped = true; + + if (this.limited) { + this.limitCropBox(true, true); + } + } + + break; + + // No default + } + + if (renderable) { + cropBox.width = width; + cropBox.height = height; + cropBox.left = left; + cropBox.top = top; + this.action = action; + + this.renderCropBox(); + } + + // Override + this.startX = this.endX; + this.startY = this.endY; + }, + + // Show the crop box manually + crop: function () { + if (!this.isBuilt || this.isDisabled) { + return; + } + + if (!this.isCropped) { + this.isCropped = true; + this.limitCropBox(true, true); + + if (this.options.modal) { + this.$dragBox.addClass(CLASS_MODAL); + } + + this.$cropBox.removeClass(CLASS_HIDDEN); + } + + this.setCropBoxData(this.initialCropBox); + }, + + // Reset the image and crop box to their initial states + reset: function () { + if (!this.isBuilt || this.isDisabled) { + return; + } + + this.image = $.extend({}, this.initialImage); + this.canvas = $.extend({}, this.initialCanvas); + this.cropBox = $.extend({}, this.initialCropBox); + + this.renderCanvas(); + + if (this.isCropped) { + this.renderCropBox(); + } + }, + + // Clear the crop box + clear: function () { + if (!this.isCropped || this.isDisabled) { + return; + } + + $.extend(this.cropBox, { + left: 0, + top: 0, + width: 0, + height: 0 + }); + + this.isCropped = false; + this.renderCropBox(); + + this.limitCanvas(true, true); + + // Render canvas after crop box rendered + this.renderCanvas(); + + this.$dragBox.removeClass(CLASS_MODAL); + this.$cropBox.addClass(CLASS_HIDDEN); + }, + + /** + * Replace the image's src and rebuild the cropper + * + * @param {String} url + * @param {Boolean} onlyColorChanged (optional) + */ + replace: function (url, onlyColorChanged) { + if (!this.isDisabled && url) { + if (this.isImg) { + this.$element.attr('src', url); + } + + if (onlyColorChanged) { + this.url = url; + this.$clone.attr('src', url); + + if (this.isBuilt) { + this.$preview.find('img').add(this.$clone2).attr('src', url); + } + } else { + if (this.isImg) { + this.isReplaced = true; + } + + // Clear previous data + this.options.data = null; + this.load(url); + } + } + }, + + // Enable (unfreeze) the cropper + enable: function () { + if (this.isBuilt) { + this.isDisabled = false; + this.$cropper.removeClass(CLASS_DISABLED); + } + }, + + // Disable (freeze) the cropper + disable: function () { + if (this.isBuilt) { + this.isDisabled = true; + this.$cropper.addClass(CLASS_DISABLED); + } + }, + + // Destroy the cropper and remove the instance from the image + destroy: function () { + var $this = this.$element; + + if (this.isLoaded) { + if (this.isImg && this.isReplaced) { + $this.attr('src', this.originalUrl); + } + + this.unbuild(); + $this.removeClass(CLASS_HIDDEN); + } else { + if (this.isImg) { + $this.off(EVENT_LOAD, this.start); + } else if (this.$clone) { + this.$clone.remove(); + } + } + + $this.removeData(NAMESPACE); + }, + + /** + * Move the canvas with relative offsets + * + * @param {Number} offsetX + * @param {Number} offsetY (optional) + */ + move: function (offsetX, offsetY) { + var canvas = this.canvas; + + this.moveTo( + isUndefined(offsetX) ? offsetX : canvas.left + num(offsetX), + isUndefined(offsetY) ? offsetY : canvas.top + num(offsetY) + ); + }, + + /** + * Move the canvas to an absolute point + * + * @param {Number} x + * @param {Number} y (optional) + */ + moveTo: function (x, y) { + var canvas = this.canvas; + var isChanged = false; + + // If "y" is not present, its default value is "x" + if (isUndefined(y)) { + y = x; + } + + x = num(x); + y = num(y); + + if (this.isBuilt && !this.isDisabled && this.options.movable) { + if (isNumber(x)) { + canvas.left = x; + isChanged = true; + } + + if (isNumber(y)) { + canvas.top = y; + isChanged = true; + } + + if (isChanged) { + this.renderCanvas(true); + } + } + }, + + /** + * Zoom the canvas with a relative ratio + * + * @param {Number} ratio + * @param {jQuery Event} _event (private) + */ + zoom: function (ratio, _event) { + var canvas = this.canvas; + + ratio = num(ratio); + + if (ratio < 0) { + ratio = 1 / (1 - ratio); + } else { + ratio = 1 + ratio; + } + + this.zoomTo(canvas.width * ratio / canvas.naturalWidth, _event); + }, + + /** + * Zoom the canvas to an absolute ratio + * + * @param {Number} ratio + * @param {jQuery Event} _event (private) + */ + zoomTo: function (ratio, _event) { + var options = this.options; + var canvas = this.canvas; + var width = canvas.width; + var height = canvas.height; + var naturalWidth = canvas.naturalWidth; + var naturalHeight = canvas.naturalHeight; + var originalEvent; + var newWidth; + var newHeight; + var offset; + var center; + + ratio = num(ratio); + + if (ratio >= 0 && this.isBuilt && !this.isDisabled && options.zoomable) { + newWidth = naturalWidth * ratio; + newHeight = naturalHeight * ratio; + + if (_event) { + originalEvent = _event.originalEvent; + } + + if (this.trigger(EVENT_ZOOM, { + originalEvent: originalEvent, + oldRatio: width / naturalWidth, + ratio: newWidth / naturalWidth + }).isDefaultPrevented()) { + return; + } + + if (originalEvent) { + offset = this.$cropper.offset(); + center = originalEvent.touches ? getTouchesCenter(originalEvent.touches) : { + pageX: _event.pageX || originalEvent.pageX || 0, + pageY: _event.pageY || originalEvent.pageY || 0 + }; + + // Zoom from the triggering point of the event + canvas.left -= (newWidth - width) * ( + ((center.pageX - offset.left) - canvas.left) / width + ); + canvas.top -= (newHeight - height) * ( + ((center.pageY - offset.top) - canvas.top) / height + ); + } else { + + // Zoom from the center of the canvas + canvas.left -= (newWidth - width) / 2; + canvas.top -= (newHeight - height) / 2; + } + + canvas.width = newWidth; + canvas.height = newHeight; + this.renderCanvas(true); + } + }, + + /** + * Rotate the canvas with a relative degree + * + * @param {Number} degree + */ + rotate: function (degree) { + this.rotateTo((this.image.rotate || 0) + num(degree)); + }, + + /** + * Rotate the canvas to an absolute degree + * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#rotate() + * + * @param {Number} degree + */ + rotateTo: function (degree) { + degree = num(degree); + + if (isNumber(degree) && this.isBuilt && !this.isDisabled && this.options.rotatable) { + this.image.rotate = degree % 360; + this.isRotated = true; + this.renderCanvas(true); + } + }, + + /** + * Scale the image + * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#scale() + * + * @param {Number} scaleX + * @param {Number} scaleY (optional) + */ + scale: function (scaleX, scaleY) { + var image = this.image; + var isChanged = false; + + // If "scaleY" is not present, its default value is "scaleX" + if (isUndefined(scaleY)) { + scaleY = scaleX; + } + + scaleX = num(scaleX); + scaleY = num(scaleY); + + if (this.isBuilt && !this.isDisabled && this.options.scalable) { + if (isNumber(scaleX)) { + image.scaleX = scaleX; + isChanged = true; + } + + if (isNumber(scaleY)) { + image.scaleY = scaleY; + isChanged = true; + } + + if (isChanged) { + this.renderImage(true); + } + } + }, + + /** + * Scale the abscissa of the image + * + * @param {Number} scaleX + */ + scaleX: function (scaleX) { + var scaleY = this.image.scaleY; + + this.scale(scaleX, isNumber(scaleY) ? scaleY : 1); + }, + + /** + * Scale the ordinate of the image + * + * @param {Number} scaleY + */ + scaleY: function (scaleY) { + var scaleX = this.image.scaleX; + + this.scale(isNumber(scaleX) ? scaleX : 1, scaleY); + }, + + /** + * Get the cropped area position and size data (base on the original image) + * + * @param {Boolean} isRounded (optional) + * @return {Object} data + */ + getData: function (isRounded) { + var options = this.options; + var image = this.image; + var canvas = this.canvas; + var cropBox = this.cropBox; + var ratio; + var data; + + if (this.isBuilt && this.isCropped) { + data = { + x: cropBox.left - canvas.left, + y: cropBox.top - canvas.top, + width: cropBox.width, + height: cropBox.height + }; + + ratio = image.width / image.naturalWidth; + + $.each(data, function (i, n) { + n = n / ratio; + data[i] = isRounded ? round(n) : n; + }); + + } else { + data = { + x: 0, + y: 0, + width: 0, + height: 0 + }; + } + + if (options.rotatable) { + data.rotate = image.rotate || 0; + } + + if (options.scalable) { + data.scaleX = image.scaleX || 1; + data.scaleY = image.scaleY || 1; + } + + return data; + }, + + /** + * Set the cropped area position and size with new data + * + * @param {Object} data + */ + setData: function (data) { + var options = this.options; + var image = this.image; + var canvas = this.canvas; + var cropBoxData = {}; + var isRotated; + var isScaled; + var ratio; + + if ($.isFunction(data)) { + data = data.call(this.element); + } + + if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) { + if (options.rotatable) { + if (isNumber(data.rotate) && data.rotate !== image.rotate) { + image.rotate = data.rotate; + this.isRotated = isRotated = true; + } + } + + if (options.scalable) { + if (isNumber(data.scaleX) && data.scaleX !== image.scaleX) { + image.scaleX = data.scaleX; + isScaled = true; + } + + if (isNumber(data.scaleY) && data.scaleY !== image.scaleY) { + image.scaleY = data.scaleY; + isScaled = true; + } + } + + if (isRotated) { + this.renderCanvas(); + } else if (isScaled) { + this.renderImage(); + } + + ratio = image.width / image.naturalWidth; + + if (isNumber(data.x)) { + cropBoxData.left = data.x * ratio + canvas.left; + } + + if (isNumber(data.y)) { + cropBoxData.top = data.y * ratio + canvas.top; + } + + if (isNumber(data.width)) { + cropBoxData.width = data.width * ratio; + } + + if (isNumber(data.height)) { + cropBoxData.height = data.height * ratio; + } + + this.setCropBoxData(cropBoxData); + } + }, + + /** + * Get the container size data + * + * @return {Object} data + */ + getContainerData: function () { + return this.isBuilt ? this.container : {}; + }, + + /** + * Get the image position and size data + * + * @return {Object} data + */ + getImageData: function () { + return this.isLoaded ? this.image : {}; + }, + + /** + * Get the canvas position and size data + * + * @return {Object} data + */ + getCanvasData: function () { + var canvas = this.canvas; + var data = {}; + + if (this.isBuilt) { + $.each([ + 'left', + 'top', + 'width', + 'height', + 'naturalWidth', + 'naturalHeight' + ], function (i, n) { + data[n] = canvas[n]; + }); + } + + return data; + }, + + /** + * Set the canvas position and size with new data + * + * @param {Object} data + */ + setCanvasData: function (data) { + var canvas = this.canvas; + var aspectRatio = canvas.aspectRatio; + + if ($.isFunction(data)) { + data = data.call(this.$element); + } + + if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) { + if (isNumber(data.left)) { + canvas.left = data.left; + } + + if (isNumber(data.top)) { + canvas.top = data.top; + } + + if (isNumber(data.width)) { + canvas.width = data.width; + canvas.height = data.width / aspectRatio; + } else if (isNumber(data.height)) { + canvas.height = data.height; + canvas.width = data.height * aspectRatio; + } + + this.renderCanvas(true); + } + }, + + /** + * Get the crop box position and size data + * + * @return {Object} data + */ + getCropBoxData: function () { + var cropBox = this.cropBox; + var data; + + if (this.isBuilt && this.isCropped) { + data = { + left: cropBox.left, + top: cropBox.top, + width: cropBox.width, + height: cropBox.height + }; + } + + return data || {}; + }, + + /** + * Set the crop box position and size with new data + * + * @param {Object} data + */ + setCropBoxData: function (data) { + var cropBox = this.cropBox; + var aspectRatio = this.options.aspectRatio; + var isWidthChanged; + var isHeightChanged; + + if ($.isFunction(data)) { + data = data.call(this.$element); + } + + if (this.isBuilt && this.isCropped && !this.isDisabled && $.isPlainObject(data)) { + + if (isNumber(data.left)) { + cropBox.left = data.left; + } + + if (isNumber(data.top)) { + cropBox.top = data.top; + } + + if (isNumber(data.width)) { + isWidthChanged = true; + cropBox.width = data.width; + } + + if (isNumber(data.height)) { + isHeightChanged = true; + cropBox.height = data.height; + } + + if (aspectRatio) { + if (isWidthChanged) { + cropBox.height = cropBox.width / aspectRatio; + } else if (isHeightChanged) { + cropBox.width = cropBox.height * aspectRatio; + } + } + + this.renderCropBox(); + } + }, + + /** + * Get a canvas drawn the cropped image + * + * @param {Object} options (optional) + * @return {HTMLCanvasElement} canvas + */ + getCroppedCanvas: function (options) { + var originalWidth; + var originalHeight; + var canvasWidth; + var canvasHeight; + var scaledWidth; + var scaledHeight; + var scaledRatio; + var aspectRatio; + var canvas; + var context; + var data; + + if (!this.isBuilt || !this.isCropped || !SUPPORT_CANVAS) { + return; + } + + if (!$.isPlainObject(options)) { + options = {}; + } + + data = this.getData(); + originalWidth = data.width; + originalHeight = data.height; + aspectRatio = originalWidth / originalHeight; + + if ($.isPlainObject(options)) { + scaledWidth = options.width; + scaledHeight = options.height; + + if (scaledWidth) { + scaledHeight = scaledWidth / aspectRatio; + scaledRatio = scaledWidth / originalWidth; + } else if (scaledHeight) { + scaledWidth = scaledHeight * aspectRatio; + scaledRatio = scaledHeight / originalHeight; + } + } + + // The canvas element will use `Math.floor` on a float number, so floor first + canvasWidth = floor(scaledWidth || originalWidth); + canvasHeight = floor(scaledHeight || originalHeight); + + canvas = $('')[0]; + canvas.width = canvasWidth; + canvas.height = canvasHeight; + context = canvas.getContext('2d'); + + if (options.fillColor) { + context.fillStyle = options.fillColor; + context.fillRect(0, 0, canvasWidth, canvasHeight); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage + context.drawImage.apply(context, (function () { + var source = getSourceCanvas(this.$clone[0], this.image); + var sourceWidth = source.width; + var sourceHeight = source.height; + var canvas = this.canvas; + var params = [source]; + + // Source canvas + var srcX = data.x + canvas.naturalWidth * (abs(data.scaleX || 1) - 1) / 2; + var srcY = data.y + canvas.naturalHeight * (abs(data.scaleY || 1) - 1) / 2; + var srcWidth; + var srcHeight; + + // Destination canvas + var dstX; + var dstY; + var dstWidth; + var dstHeight; + + if (srcX <= -originalWidth || srcX > sourceWidth) { + srcX = srcWidth = dstX = dstWidth = 0; + } else if (srcX <= 0) { + dstX = -srcX; + srcX = 0; + srcWidth = dstWidth = min(sourceWidth, originalWidth + srcX); + } else if (srcX <= sourceWidth) { + dstX = 0; + srcWidth = dstWidth = min(originalWidth, sourceWidth - srcX); + } + + if (srcWidth <= 0 || srcY <= -originalHeight || srcY > sourceHeight) { + srcY = srcHeight = dstY = dstHeight = 0; + } else if (srcY <= 0) { + dstY = -srcY; + srcY = 0; + srcHeight = dstHeight = min(sourceHeight, originalHeight + srcY); + } else if (srcY <= sourceHeight) { + dstY = 0; + srcHeight = dstHeight = min(originalHeight, sourceHeight - srcY); + } + + // All the numerical parameters should be integer for `drawImage` (#476) + params.push(floor(srcX), floor(srcY), floor(srcWidth), floor(srcHeight)); + + // Scale destination sizes + if (scaledRatio) { + dstX *= scaledRatio; + dstY *= scaledRatio; + dstWidth *= scaledRatio; + dstHeight *= scaledRatio; + } + + // Avoid "IndexSizeError" in IE and Firefox + if (dstWidth > 0 && dstHeight > 0) { + params.push(floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight)); + } + + return params; + }).call(this)); + + return canvas; + }, + + /** + * Change the aspect ratio of the crop box + * + * @param {Number} aspectRatio + */ + setAspectRatio: function (aspectRatio) { + var options = this.options; + + if (!this.isDisabled && !isUndefined(aspectRatio)) { + + // 0 -> NaN + options.aspectRatio = max(0, aspectRatio) || NaN; + + if (this.isBuilt) { + this.initCropBox(); + + if (this.isCropped) { + this.renderCropBox(); + } + } + } + }, + + /** + * Change the drag mode + * + * @param {String} mode (optional) + */ + setDragMode: function (mode) { + var options = this.options; + var croppable; + var movable; + + if (this.isLoaded && !this.isDisabled) { + croppable = mode === ACTION_CROP; + movable = options.movable && mode === ACTION_MOVE; + mode = (croppable || movable) ? mode : ACTION_NONE; + + this.$dragBox. + data(DATA_ACTION, mode). + toggleClass(CLASS_CROP, croppable). + toggleClass(CLASS_MOVE, movable); + + if (!options.cropBoxMovable) { + + // Sync drag mode to crop box when it is not movable(#300) + this.$face. + data(DATA_ACTION, mode). + toggleClass(CLASS_CROP, croppable). + toggleClass(CLASS_MOVE, movable); + } + } + } + }; + + Cropper.DEFAULTS = { + + // Define the view mode of the cropper + viewMode: 0, // 0, 1, 2, 3 + + // Define the dragging mode of the cropper + dragMode: 'crop', // 'crop', 'move' or 'none' + + // Define the aspect ratio of the crop box + aspectRatio: NaN, + + // An object with the previous cropping result data + data: null, + + // A jQuery selector for adding extra containers to preview + preview: '', + + // Re-render the cropper when resize the window + responsive: true, + + // Restore the cropped area after resize the window + restore: true, + + // Check if the current image is a cross-origin image + checkCrossOrigin: true, + + // Check the current image's Exif Orientation information + checkOrientation: true, + + // Show the black modal + modal: true, + + // Show the dashed lines for guiding + guides: true, + + // Show the center indicator for guiding + center: true, + + // Show the white modal to highlight the crop box + highlight: true, + + // Show the grid background + background: true, + + // Enable to crop the image automatically when initialize + autoCrop: true, + + // Define the percentage of automatic cropping area when initializes + autoCropArea: 0.8, + + // Enable to move the image + movable: true, + + // Enable to rotate the image + rotatable: true, + + // Enable to scale the image + scalable: true, + + // Enable to zoom the image + zoomable: true, + + // Enable to zoom the image by dragging touch + zoomOnTouch: true, + + // Enable to zoom the image by wheeling mouse + zoomOnWheel: true, + + // Define zoom ratio when zoom the image by wheeling mouse + wheelZoomRatio: 0.1, + + // Enable to move the crop box + cropBoxMovable: true, + + // Enable to resize the crop box + cropBoxResizable: true, + + // Toggle drag mode between "crop" and "move" when click twice on the cropper + toggleDragModeOnDblclick: true, + + // Size limitation + minCanvasWidth: 0, + minCanvasHeight: 0, + minCropBoxWidth: 0, + minCropBoxHeight: 0, + minContainerWidth: 200, + minContainerHeight: 100, + + // Shortcuts of events + build: null, + built: null, + cropstart: null, + cropmove: null, + cropend: null, + crop: null, + zoom: null + }; + + Cropper.setDefaults = function (options) { + $.extend(Cropper.DEFAULTS, options); + }; + + Cropper.TEMPLATE = ( + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
    ' + + '
    ' + ); + + // Save the other cropper + Cropper.other = $.fn.cropper; + + // Register as jQuery plugin + $.fn.cropper = function (option) { + var args = toArray(arguments, 1); + var result; + + this.each(function () { + var $this = $(this); + var data = $this.data(NAMESPACE); + var options; + var fn; + + if (!data) { + if (/destroy/.test(option)) { + return; + } + + options = $.extend({}, $this.data(), $.isPlainObject(option) && option); + $this.data(NAMESPACE, (data = new Cropper(this, options))); + } + + if (typeof option === 'string' && $.isFunction(fn = data[option])) { + result = fn.apply(data, args); + } + }); + + return isUndefined(result) ? this : result; + }; + + $.fn.cropper.Constructor = Cropper; + $.fn.cropper.setDefaults = Cropper.setDefaults; + + // No conflict + $.fn.cropper.noConflict = function () { + $.fn.cropper = Cropper.other; + return this; + }; + +}); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 2d301d21ab..e2d590f4df 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -9,6 +9,7 @@ *= require_self *= require dropzone/basic *= require cal-heatmap + *= require cropper.css */ /* diff --git a/app/assets/stylesheets/cropper.css b/app/assets/stylesheets/cropper.css new file mode 100644 index 0000000000..8668c7c049 --- /dev/null +++ b/app/assets/stylesheets/cropper.css @@ -0,0 +1,379 @@ +/*! + * Cropper v2.3.0 + * https://github.com/fengyuanchen/cropper + * + * Copyright (c) 2014-2016 Fengyuan Chen and contributors + * Released under the MIT license + * + * Date: 2016-02-22T02:13:13.332Z + */ +.cropper-container { + font-size: 0; + line-height: 0; + + position: relative; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + direction: ltr !important; + -ms-touch-action: none; + touch-action: none; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; +} + +.cropper-container img { + display: block; + + width: 100%; + min-width: 0 !important; + max-width: none !important; + height: 100%; + min-height: 0 !important; + max-height: none !important; + + image-orientation: 0deg !important; +} + +.cropper-wrap-box, +.cropper-canvas, +.cropper-drag-box, +.cropper-crop-box, +.cropper-modal { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.cropper-wrap-box { + overflow: hidden; +} + +.cropper-drag-box { + opacity: 0; + background-color: #fff; + + filter: alpha(opacity=0); +} + +.cropper-modal { + opacity: .5; + background-color: #000; + + filter: alpha(opacity=50); +} + +.cropper-view-box { + display: block; + overflow: hidden; + + width: 100%; + height: 100%; + + outline: 1px solid #39f; + outline-color: rgba(51, 153, 255, .75); +} + +.cropper-dashed { + position: absolute; + + display: block; + + opacity: .5; + border: 0 dashed #eee; + + filter: alpha(opacity=50); +} + +.cropper-dashed.dashed-h { + top: 33.33333%; + left: 0; + + width: 100%; + height: 33.33333%; + + border-top-width: 1px; + border-bottom-width: 1px; +} + +.cropper-dashed.dashed-v { + top: 0; + left: 33.33333%; + + width: 33.33333%; + height: 100%; + + border-right-width: 1px; + border-left-width: 1px; +} + +.cropper-center { + position: absolute; + top: 50%; + left: 50%; + + display: block; + + width: 0; + height: 0; + + opacity: .75; + + filter: alpha(opacity=75); +} + +.cropper-center:before, +.cropper-center:after { + position: absolute; + + display: block; + + content: ' '; + + background-color: #eee; +} + +.cropper-center:before { + top: 0; + left: -3px; + + width: 7px; + height: 1px; +} + +.cropper-center:after { + top: -3px; + left: 0; + + width: 1px; + height: 7px; +} + +.cropper-face, +.cropper-line, +.cropper-point { + position: absolute; + + display: block; + + width: 100%; + height: 100%; + + opacity: .1; + + filter: alpha(opacity=10); +} + +.cropper-face { + top: 0; + left: 0; + + background-color: #fff; +} + +.cropper-line { + background-color: #39f; +} + +.cropper-line.line-e { + top: 0; + right: -3px; + + width: 5px; + + cursor: e-resize; +} + +.cropper-line.line-n { + top: -3px; + left: 0; + + height: 5px; + + cursor: n-resize; +} + +.cropper-line.line-w { + top: 0; + left: -3px; + + width: 5px; + + cursor: w-resize; +} + +.cropper-line.line-s { + bottom: -3px; + left: 0; + + height: 5px; + + cursor: s-resize; +} + +.cropper-point { + width: 5px; + height: 5px; + + opacity: .75; + background-color: #39f; + + filter: alpha(opacity=75); +} + +.cropper-point.point-e { + top: 50%; + right: -3px; + + margin-top: -3px; + + cursor: e-resize; +} + +.cropper-point.point-n { + top: -3px; + left: 50%; + + margin-left: -3px; + + cursor: n-resize; +} + +.cropper-point.point-w { + top: 50%; + left: -3px; + + margin-top: -3px; + + cursor: w-resize; +} + +.cropper-point.point-s { + bottom: -3px; + left: 50%; + + margin-left: -3px; + + cursor: s-resize; +} + +.cropper-point.point-ne { + top: -3px; + right: -3px; + + cursor: ne-resize; +} + +.cropper-point.point-nw { + top: -3px; + left: -3px; + + cursor: nw-resize; +} + +.cropper-point.point-sw { + bottom: -3px; + left: -3px; + + cursor: sw-resize; +} + +.cropper-point.point-se { + right: -3px; + bottom: -3px; + + width: 20px; + height: 20px; + + cursor: se-resize; + + opacity: 1; + + filter: alpha(opacity=100); +} + +.cropper-point.point-se:before { + position: absolute; + right: -50%; + bottom: -50%; + + display: block; + + width: 200%; + height: 200%; + + content: ' '; + + opacity: 0; + background-color: #39f; + + filter: alpha(opacity=0); +} + +@media (min-width: 768px) { + .cropper-point.point-se { + width: 15px; + height: 15px; + } +} + +@media (min-width: 992px) { + .cropper-point.point-se { + width: 10px; + height: 10px; + } +} + +@media (min-width: 1200px) { + .cropper-point.point-se { + width: 5px; + height: 5px; + + opacity: .75; + + filter: alpha(opacity=75); + } +} + +.cropper-invisible { + opacity: 0; + + filter: alpha(opacity=0); +} + +.cropper-bg { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC'); +} + +.cropper-hide { + position: absolute; + + display: block; + + width: 0; + height: 0; +} + +.cropper-hidden { + display: none !important; +} + +.cropper-move { + cursor: move; +} + +.cropper-crop { + cursor: crosshair; +} + +.cropper-disabled .cropper-drag-box, +.cropper-disabled .cropper-face, +.cropper-disabled .cropper-line, +.cropper-disabled .cropper-point { + cursor: not-allowed; +} From 768008f605e59601585cc32c8f6efbfedb659c30 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 09:41:38 -0500 Subject: [PATCH 022/618] Respond to json requests --- app/controllers/profiles_controller.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 32fca6b838..30e2886cac 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController def update user_params.except!(:email) if @user.ldap_user? - if @user.update_attributes(user_params) - flash[:notice] = "Profile was successfully updated" - else - messages = @user.errors.full_messages.uniq.join('. ') - flash[:alert] = "Failed to update profile. #{messages}" - end - respond_to do |format| - format.html { redirect_back_or_default(default: { action: 'show' }) } + if @user.update_attributes(user_params) + message = "Profile was successfully updated" + format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) } + format.json { render json: { message: message } } + else + message = @user.errors.full_messages.uniq.join('. ') + format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) } + format.json { render json: { message: message }, status: :unprocessable_entity } + end end end From 8ffc04ebc9723bed25721cbb84ab01f0843c7a92 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 09:44:08 -0500 Subject: [PATCH 023/618] Add GitLabCrop class --- app/assets/javascripts/gl_crop.js.coffee | 118 +++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 app/assets/javascripts/gl_crop.js.coffee diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee new file mode 100644 index 0000000000..5d5b2c406a --- /dev/null +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -0,0 +1,118 @@ +class GitLabCrop + constructor: (el, opts = {}) -> + # Input file + @fileInput = $(el) + + # Set defaults + { + @filename + @form = @fileInput.parents('form') + @modalCrop = '.modal-profile-crop' + + # Button where user clicks to open file dialog + # If not passed as argument let's pick a default one + @pickImageEl = @fileInput.parent().find('.js-choose-user-avatar-button') + @uploadImageBtn = $('.js-upload-user-avatar') + } = opts + + # Ensure @modalCrop is a jQuery Object + @modalCrop = $(@modalCrop) + @modalCropImg = $('.modal-profile-crop-image') + @cropActionsBtn = @modalCrop.find('[data-method]') + + @bindEvents() + + bindEvents: -> + self = @ + @fileInput.on 'change', (e) -> + self.onFileInputChange(e, @) + + @pickImageEl.on 'click', @onPickImageClick + @modalCrop.on 'shown.bs.modal', @onModalShow + @modalCrop.on 'hidden.bs.modal', @onModalHide + @uploadImageBtn.on 'click', @onUploadImageBtnClick + @cropActionsBtn.on 'click', (e) -> + btn = @ + self.onActionBtnClick(btn) + @croppedImageBlob = null + + onPickImageClick: => + @fileInput.trigger('click') + + onModalShow: => + @modalCropImg.cropper( + viewMode: 1 + center: false + aspectRatio: 1 + modal: true + scalable: false + rotatable: false + zoomable: true + dragMode: 'move' + guides: false + zoomOnTouch: false + zoomOnWheel: false + cropBoxMovable: false + cropBoxResizable: false + toggleDragModeOnDblclick: false + built: -> + container = $(@).cropper 'getContainerData' + cropBoxWidth = 200; + cropBoxHeight = 200; + + $(@).cropper('setCropBoxData', + width: cropBoxWidth, + height: cropBoxHeight, + left: (container.width - cropBoxWidth) / 2, + top: (container.height - cropBoxHeight) / 2 + ) + ) + + + onModalHide: => + @modalCropImg + .attr('src', '') # Remove attached image + .cropper('destroy') # Destroy cropper instance + + onUploadImageBtnClick: (e) => + e.preventDefault() + @setBlob() + @modalCrop.modal('hide') + # @form.submit(); + + onActionBtnClick: (btn) -> + data = $(btn).data() + + if @modalCropImg.data('cropper') && data.method + data = $.extend {}, data + result = @modalCropImg.cropper data.method, data.option + + onFileInputChange: (e, input) -> + @readFile(input) + + readFile: (input) -> + self = @ + reader = new FileReader + reader.onload = -> + self.modalCropImg.attr('src', reader.result) + self.modalCrop.modal('show') + + reader.readAsDataURL(input.files[0]) + + dataURLtoBlob: (dataURL) -> + binary = atob(dataURL.split(',')[1]) + array = [] + for v, k in binary + array.push(binary.charCodeAt(k)) + new Blob([new Uint8Array(array)], type: 'image/png') + + setBlob: -> + dataURL = @modalCropImg.cropper('getCroppedCanvas').toDataURL('image/png') + @croppedImageBlob = @dataURLtoBlob(dataURL) + + getBlob: -> + @croppedImageBlob + +$.fn.glCrop = (opts) -> + return @.each -> + $(@).data('glcrop', new GitLabCrop(@, opts)) From 707be371eecda44b989f672fc12950a72bd2c502 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 09:46:50 -0500 Subject: [PATCH 024/618] Handle form submit via ajax and crop avatar on the client --- app/assets/javascripts/profile.js.coffee | 43 +++++++++++++++++++----- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index 20f8744055..f81d7dd6e6 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -1,5 +1,9 @@ class @Profile - constructor: -> + constructor: (opts = {}) -> + { + @form = $('.edit-user') + } = opts + # Automatically submit the Preferences form when any of its radio buttons change $('.js-preferences-form').on 'change.preference', 'input[type=radio]', -> $(this).parents('form').submit() @@ -17,14 +21,37 @@ class @Profile $('.update-notifications').on 'ajax:complete', -> $(this).find('.btn-save').enable() - $('.js-choose-user-avatar-button').bind "click", -> - form = $(this).closest("form") - form.find(".js-user-avatar-input").click() + @bindEvents() - $('.js-user-avatar-input').bind "change", -> - form = $(this).closest("form") - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find(".js-avatar-filename").text(filename) + @avatarGlCrop = $('.js-user-avatar-input').glCrop().data 'glcrop' + + bindEvents: -> + @form.on 'submit', @onSubmitForm + + onSubmitForm: (e) => + e.preventDefault() + @saveForm() + + saveForm: -> + self = @ + + formData = new FormData(@form[0]) + formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png') + + $.ajax + url: @form.attr('action') + type: @form.attr('method') + data: formData + dataType: "json" + processData: false + contentType: false + success: (response) -> + new Flash(response.message, 'notice') + error: (jqXHR) -> + new Flash(jqXHR.responseJSON.message, 'alert') + complete: -> + window.scrollTo 0, 0 + self.form.find(':input[disabled]').enable() $ -> # Extract the SSH Key title from its comment From 7216ce97f748988c644b1baff2ec42006e508fbc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 09:48:47 -0500 Subject: [PATCH 025/618] Add modal dialog --- app/assets/stylesheets/pages/profile.scss | 17 ++++++++++++++++ app/views/profiles/show.html.haml | 24 ++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 260179074c..0cd37e7014 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -197,3 +197,20 @@ width: 105px; } } + +.modal-profile-crop { + .modal-dialog { + width: 380px; + } + + .profile-crop-image-container { + width: 350px; + height: 350px; + margin: 0 auto; + } + + .crop-controls { + padding: 10px 0 0 0; + text-align: center; + } +} diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index cd582ba706..2d9129470f 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -26,7 +26,7 @@ %a.btn.js-choose-user-avatar-button Browse file... %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen - = f.file_field :avatar, class: "js-user-avatar-input hidden" + = f.file_field :avatar_dialog_trigger, class: "js-user-avatar-input hidden", accept: "image/*" .help-block The maximum file size allowed is 200KB. - if @user.avatar? @@ -94,3 +94,25 @@ .prepend-top-default.append-bottom-default = f.submit 'Update profile settings', class: "btn btn-success" = link_to "Cancel", user_path(current_user), class: "btn btn-cancel" + +.modal.modal-profile-crop + .modal-dialog + .modal-content + .modal-header + %button.close{:type => "button", :'data-dismiss' => "modal"} + %span + × + %h4.modal-title + Position and size your new avatar + .modal-body + .profile-crop-image-container + %img.modal-profile-crop-image + .crop-controls + .btn-group + %button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } } + %span.fa.fa-search-plus + %button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } } + %span.fa.fa-search-minus + .modal-footer + %button.btn.btn-primary.js-upload-user-avatar{:type => "button"} + Set new profile picture From 95cc46a44a2d871fe700ed08850851f863ef2408 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 11:34:21 -0500 Subject: [PATCH 026/618] Allow to set crop box and export image dimensions --- app/assets/javascripts/gl_crop.js.coffee | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index 5d5b2c406a..276e56b434 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -6,8 +6,13 @@ class GitLabCrop # Set defaults { @filename + @previewImage = $('.avatar-image .avatar') @form = @fileInput.parents('form') @modalCrop = '.modal-profile-crop' + @exportWidth = 200 + @exportHeight = 200 + @cropBoxWidth = 200 + @cropBoxHeight = 200 # Button where user clicks to open file dialog # If not passed as argument let's pick a default one @@ -40,6 +45,7 @@ class GitLabCrop @fileInput.trigger('click') onModalShow: => + self = @ @modalCropImg.cropper( viewMode: 1 center: false @@ -57,8 +63,8 @@ class GitLabCrop toggleDragModeOnDblclick: false built: -> container = $(@).cropper 'getContainerData' - cropBoxWidth = 200; - cropBoxHeight = 200; + cropBoxWidth = self.cropBoxWidth; + cropBoxHeight = self.cropBoxHeight; $(@).cropper('setCropBoxData', width: cropBoxWidth, @@ -77,8 +83,8 @@ class GitLabCrop onUploadImageBtnClick: (e) => e.preventDefault() @setBlob() + @setPreview() @modalCrop.modal('hide') - # @form.submit(); onActionBtnClick: (btn) -> data = $(btn).data() @@ -106,9 +112,15 @@ class GitLabCrop array.push(binary.charCodeAt(k)) new Blob([new Uint8Array(array)], type: 'image/png') + setPreview: -> + @previewImage.attr('src', @dataURL) + setBlob: -> - dataURL = @modalCropImg.cropper('getCroppedCanvas').toDataURL('image/png') - @croppedImageBlob = @dataURLtoBlob(dataURL) + @dataURL = @modalCropImg.cropper('getCroppedCanvas', + width: 200 + height: 200 + ).toDataURL('image/png') + @croppedImageBlob = @dataURLtoBlob(@dataURL) getBlob: -> @croppedImageBlob From 358545b8d2b41b24bdb8dc581f9541122212ae56 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 11:34:31 -0500 Subject: [PATCH 027/618] Make it responsive --- app/assets/stylesheets/pages/profile.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 0cd37e7014..3831f88f01 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -201,11 +201,15 @@ .modal-profile-crop { .modal-dialog { width: 380px; + + @media (max-width: $screen-sm-min) { + width: auto; + } + } .profile-crop-image-container { - width: 350px; - height: 350px; + height: 300px; margin: 0 auto; } From 9ed1cbf2176bcab22d522373b2830338dc5c7c69 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 13:20:30 -0500 Subject: [PATCH 028/618] Show filename after setting a cropped image --- app/assets/javascripts/gl_crop.js.coffee | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index 276e56b434..4d990a6416 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -5,9 +5,9 @@ class GitLabCrop # Set defaults { - @filename - @previewImage = $('.avatar-image .avatar') @form = @fileInput.parents('form') + @filename = '.js-avatar-filename' + @previewImage = $('.avatar-image .avatar') @modalCrop = '.modal-profile-crop' @exportWidth = 200 @exportHeight = 200 @@ -20,13 +20,20 @@ class GitLabCrop @uploadImageBtn = $('.js-upload-user-avatar') } = opts - # Ensure @modalCrop is a jQuery Object - @modalCrop = $(@modalCrop) + # Ensure needed elements are jquery objects + @filename = if _.isString(@filename) then @$(@filename) else @filename + + # Modal usually is outside the wrapper element + @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop + @modalCropImg = $('.modal-profile-crop-image') @cropActionsBtn = @modalCrop.find('[data-method]') @bindEvents() + $: (selector) -> + $(selector, @form) + bindEvents: -> self = @ @fileInput.on 'change', (e) -> @@ -114,6 +121,8 @@ class GitLabCrop setPreview: -> @previewImage.attr('src', @dataURL) + filename = @fileInput.val().replace(/^.*[\\\/]/, '') + @filename.text(filename) setBlob: -> @dataURL = @modalCropImg.cropper('getCroppedCanvas', From bc70597908c2aa144472b28c891306a6d9390c34 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 14:30:38 -0500 Subject: [PATCH 029/618] Fix failing spec --- app/assets/javascripts/gl_crop.js.coffee | 11 ++++++++--- app/views/profiles/show.html.haml | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index 4d990a6416..6ab0afc184 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -1,7 +1,12 @@ class GitLabCrop - constructor: (el, opts = {}) -> - # Input file - @fileInput = $(el) + constructor: (input, opts = {}) -> + @fileInput = $(input) + + # We should rename to avoid spec to fail + # Form will submit the proper input filed with a file using FormData + @fileInput + .attr('name', "#{@fileInput.attr('name')}-trigger") + .attr('id', "#{@fileInput.attr('id')}-trigger") # Set defaults { diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 2d9129470f..dcb3be9585 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -26,7 +26,7 @@ %a.btn.js-choose-user-avatar-button Browse file... %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen - = f.file_field :avatar_dialog_trigger, class: "js-user-avatar-input hidden", accept: "image/*" + = f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*" .help-block The maximum file size allowed is 200KB. - if @user.avatar? From 647f28bd1d92eabe239a7f0f4a65eb100c0cda73 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 17 Mar 2016 14:59:43 -0500 Subject: [PATCH 030/618] Make it generic --- app/assets/javascripts/gl_crop.js.coffee | 26 ++++++++++++++---------- app/assets/javascripts/profile.js.coffee | 10 ++++++++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index 6ab0afc184..137d025ed2 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -10,28 +10,32 @@ class GitLabCrop # Set defaults { - @form = @fileInput.parents('form') - @filename = '.js-avatar-filename' - @previewImage = $('.avatar-image .avatar') - @modalCrop = '.modal-profile-crop' @exportWidth = 200 @exportHeight = 200 @cropBoxWidth = 200 @cropBoxHeight = 200 + @form = @fileInput.parents('form') - # Button where user clicks to open file dialog - # If not passed as argument let's pick a default one - @pickImageEl = @fileInput.parent().find('.js-choose-user-avatar-button') - @uploadImageBtn = $('.js-upload-user-avatar') + # Required params + @filename + @previewImage + @modalCrop + @pickImageEl + @uploadImageBtn + @modalCropImg } = opts # Ensure needed elements are jquery objects - @filename = if _.isString(@filename) then @$(@filename) else @filename + # If selector is provided we will convert them to a jQuery Object + @filename = @$(@filename) + @previewImage = @$(@previewImage) + @pickImageEl = @$(@pickImageEl) - # Modal usually is outside the wrapper element + # Modal elements usually are outside the @form element @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop + @uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn + @modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg - @modalCropImg = $('.modal-profile-crop-image') @cropActionsBtn = @modalCrop.find('[data-method]') @bindEvents() diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index f81d7dd6e6..2fcc6dfd56 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -23,7 +23,15 @@ class @Profile @bindEvents() - @avatarGlCrop = $('.js-user-avatar-input').glCrop().data 'glcrop' + cropOpts = + filename: '.js-avatar-filename' + previewImage: '.avatar-image .avatar' + modalCrop: '.modal-profile-crop' + pickImageEl: '.js-choose-user-avatar-button' + uploadImageBtn: '.js-upload-user-avatar' + modalCropImg: '.modal-profile-crop-image' + + @avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop' bindEvents: -> @form.on 'submit', @onSubmitForm From efa1bc372fbfbf0503b7452e17c7034c512e19bc Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 17 Mar 2016 13:20:05 +0200 Subject: [PATCH 031/618] Fix typos and add a paragraph to urge people to mention the relevant MR [ci skip] --- CONTRIBUTING.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a946c2cd88..5d4aa61fb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -245,22 +245,25 @@ addressed. ### Technical debt -In order to track things that can be improved in GitLab codebase, we created a -*technical debt* label in [issue tracker of CE][ce-tracker]. +In order to track things that can be improved in GitLab's codebase, we created +the ~"Technical debt" label in [GitLab's issue tracker][ce-tracker]. This label should be added to issues that describe things that can be improved, -shortcuts that has been taken, code that needs refactoring, features that need +shortcuts that have been taken, code that needs refactoring, features that need additional attention, and all other things that have been left behind due to high velocity of development. -Everyone can create an issue (though you may need to ask for adding a specific -label, if you do not have permissions to do it by yourself), additional labels -can be combined with *technical debt* label, to make it easier to schedule the -improvements for a release. +Everyone can create an issue, though you may need to ask for adding a specific +label, if you do not have permissions to do it by yourself. Additional labels +can be combined with the `Technical debt` label, to make it easier to schedule +the improvements for a release. -Issues with *technical debt* label have a same priority like issues that -describe a new features that can be introduced in GitLab, and should be -scheduled for a release by appropriate person. +Issues tagged with the `Technical debt` label have the same priority like issues +that describe a new feature to be introduced in GitLab, and should be scheduled +for a release by the appropriate person. + +Make sure to mention the merge request that the `Technical debt` issue is +associated with in the description of the issue. ## Merge requests From 91880e13df19ed312bfa0a2e06743dd8a71aa1ad Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Wed, 20 Jan 2016 18:54:06 -0500 Subject: [PATCH 032/618] 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 738ffc8343..98f200f9b8 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 61b82c9db4..861ae7ee2f 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 a489d4f9b2..a86677c23a 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 033/618] 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 98f200f9b8..b1daa1f34e 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 861ae7ee2f..259e25c91a 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 a86677c23a..268171fde0 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 2ae282f48a..312d1ba35a 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 034/618] 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 0000000000..26924d87d6 --- /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 b1daa1f34e..4e42276354 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 259e25c91a..987b3e1c5b 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 b05ab86921..ccb2f9fa77 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 268171fde0..73ec56d170 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 312d1ba35a..2ae282f48a 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 035/618] 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 26924d87d6..2cb4481fa0 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 73ec56d170..ac7daa54eb 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 036/618] 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 4e42276354..bebedeca28 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 ac7daa54eb..fd45b1b978 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 037/618] 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 bebedeca28..97198e1424 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 fd45b1b978..dbc6d6c3f9 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 038/618] 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 97198e1424..27537d7266 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 dbc6d6c3f9..9537eda5aa 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 039/618] 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 2cb4481fa0..6b9fb9deb3 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 040/618] 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 6b9fb9deb3..f28fe8bbc6 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 987b3e1c5b..e57471decc 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 041/618] 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 27537d7266..168de57288 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 9537eda5aa..b5591416a3 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 042/618] 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 e57471decc..e40ec38fbf 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 043/618] 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 f28fe8bbc6..bd409faba9 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 168de57288..9afb6a0ce8 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 e40ec38fbf..2cc94596d2 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 b5591416a3..8193bb4d18 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 044/618] 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 9afb6a0ce8..b74b8c21fd 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 2cc94596d2..728d743045 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 8193bb4d18..6507c534a0 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 045/618] 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 bd409faba9..3f9ca39912 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 b74b8c21fd..0bb95e9215 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 ccb2f9fa77..2ee8e2de0e 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 6507c534a0..2be06aebe6 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 046/618] 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 0bb95e9215..877e85a12e 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 691d5db5986f8f57d187d3da19c69ef92087fe75 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Fri, 18 Mar 2016 12:11:41 +0000 Subject: [PATCH 047/618] Change capitalization --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d4aa61fb1..511336f384 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -246,7 +246,7 @@ addressed. ### Technical debt In order to track things that can be improved in GitLab's codebase, we created -the ~"Technical debt" label in [GitLab's issue tracker][ce-tracker]. +the ~"technical debt" label in [GitLab's issue tracker][ce-tracker]. This label should be added to issues that describe things that can be improved, shortcuts that have been taken, code that needs refactoring, features that need @@ -255,14 +255,14 @@ high velocity of development. Everyone can create an issue, though you may need to ask for adding a specific label, if you do not have permissions to do it by yourself. Additional labels -can be combined with the `Technical debt` label, to make it easier to schedule +can be combined with the `technical debt` label, to make it easier to schedule the improvements for a release. -Issues tagged with the `Technical debt` label have the same priority like issues +Issues tagged with the `technical debt` label have the same priority like issues that describe a new feature to be introduced in GitLab, and should be scheduled for a release by the appropriate person. -Make sure to mention the merge request that the `Technical debt` issue is +Make sure to mention the merge request that the `technical debt` issue is associated with in the description of the issue. ## Merge requests From 4366af51558f667b4d5882e4f01701ec993e12cc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 18 Mar 2016 12:54:10 +0000 Subject: [PATCH 048/618] Added JS to fix dropdown alignment Closes #14386 --- app/assets/javascripts/dropdowns.js.coffee | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 app/assets/javascripts/dropdowns.js.coffee diff --git a/app/assets/javascripts/dropdowns.js.coffee b/app/assets/javascripts/dropdowns.js.coffee new file mode 100644 index 0000000000..715ac644bd --- /dev/null +++ b/app/assets/javascripts/dropdowns.js.coffee @@ -0,0 +1,10 @@ +$ -> + $('[data-toggle="dropdown"]').each -> + $dropdown = $(@).parent() + $menu = $dropdown.find('.dropdown-menu') + + $dropdown.on 'shown.bs.dropdown', -> + dropdownRight = $menu.offset().left + $menu.outerWidth() + + if dropdownRight >= $(window).width() + $menu.addClass 'dropdown-menu-align-right' From c9e202c1d7cab4918e8b3789e9e6a553c30ead2a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 8 Mar 2016 02:56:43 -0500 Subject: [PATCH 049/618] 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 f5e1ca9860..a022c207d0 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 0000000000..490032dc78 --- /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 c180136526..df31b07910 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 494dad0b41..9102fd6d50 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 54af2c3063..c500289383 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 0000000000..dfe8bc010d --- /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 050/618] 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 490032dc78..c85fabbcd5 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 df31b07910..a6d5ab6523 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 4cb4129b71..91b6451e68 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 71a7ecab8e..a6c9fce5b8 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 525ed81b05..e0d655d305 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 b6e4502464..57b88268c0 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 9102fd6d50..cbead1b8b7 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 c500289383..843c833b4f 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 dfe8bc010d..f1ecc060cf 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 051/618] 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 a6d5ab6523..3cedf1c7b1 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 052/618] 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 3cedf1c7b1..0c4876358b 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 843c833b4f..58a3cdf955 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 053/618] 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 0000000000..3dd110cfa0 --- /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 57b88268c0..bcbdbb07ed 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 58a3cdf955..a004908fb6 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 054/618] 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 0c4876358b..b867190086 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 cbead1b8b7..de16454739 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 055/618] 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 3dd110cfa0..0000000000 --- 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 bcbdbb07ed..fa45d750f2 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 056/618] 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 c81e8bf760..36b41072eb 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 057/618] 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 36b41072eb..cdde52a38a 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 058/618] 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 cdde52a38a..4e733165b4 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 059/618] 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 4e733165b4..78e19064e5 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 060/618] 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 78e19064e5..e1fae7bbc5 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 061/618] 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 e1fae7bbc5..53bbbfbaf5 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 062/618] 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 53bbbfbaf5..3d2e3f3dbb 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 b867190086..e21a140b2a 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 e0d655d305..7af307940d 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 fa45d750f2..4a02f75719 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 a004908fb6..f051e7a186 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 063/618] 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 c85fabbcd5..0000000000 --- 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 064/618] 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 3d2e3f3dbb..ca7b22bd81 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 065/618] 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 7af307940d..eb3fbd9155 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 066/618] 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 ca7b22bd81..d9a4cb1771 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 e21a140b2a..18fa3b86d1 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 067/618] 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 d9a4cb1771..f74da006e6 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 068/618] 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 18fa3b86d1..25db343ca4 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 069/618] 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 877e85a12e..43671ee393 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 2ee8e2de0e..2ec0d20a87 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 070/618] 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 43671ee393..7102a0673e 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 071/618] 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 25db343ca4..e99a221222 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 072/618] 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 e99a221222..69ca0d996d 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 f1ecc060cf..489c0e11d0 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 073/618] 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 be626678bd..9d820a46cf 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 4a02f75719..110258a9e1 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 074/618] 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 69ca0d996d..8165502914 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 075/618] 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 8165502914..df6cb4f2c1 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 076/618] 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 df6cb4f2c1..fc8595f60c 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 9d820a46cf..8f260f24c4 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 110258a9e1..4179d0adb3 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 f051e7a186..0a5c145029 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 489c0e11d0..0000000000 --- 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 077/618] 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 fc8595f60c..a8ae261c4d 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 078/618] 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 de16454739..e6aa21887e 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 57749022b6e07e8c6af37b6ad39dd68922828315 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 22 Mar 2016 08:45:24 +0000 Subject: [PATCH 079/618] Removed dropdown JS to align instead favours HTML class --- app/assets/javascripts/dropdowns.js.coffee | 10 ---------- app/views/projects/branches/index.html.haml | 2 +- app/views/projects/tags/_download.html.haml | 2 +- 3 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 app/assets/javascripts/dropdowns.js.coffee diff --git a/app/assets/javascripts/dropdowns.js.coffee b/app/assets/javascripts/dropdowns.js.coffee deleted file mode 100644 index 715ac644bd..0000000000 --- a/app/assets/javascripts/dropdowns.js.coffee +++ /dev/null @@ -1,10 +0,0 @@ -$ -> - $('[data-toggle="dropdown"]').each -> - $dropdown = $(@).parent() - $menu = $dropdown.find('.dropdown-menu') - - $dropdown.on 'shown.bs.dropdown', -> - dropdownRight = $menu.offset().left + $menu.outerWidth() - - if dropdownRight >= $(window).width() - $menu.addClass 'dropdown-menu-align-right' diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 7afea5a504..88266e2123 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -16,7 +16,7 @@ - else Name %b.caret - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-align-right %li = link_to namespace_project_branches_path(sort: nil) do Name diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml index 667057ef2d..093d1d1bb0 100644 --- a/app/views/projects/tags/_download.html.haml +++ b/app/views/projects/tags/_download.html.haml @@ -6,7 +6,7 @@ %span.caret %span.sr-only Select Archive Format - %ul.col-xs-10.dropdown-menu{ role: 'menu' } + %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %li = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do %i.fa.fa-download From fa4126acffdfe13741e05a60ad5ed7fd407b4f16 Mon Sep 17 00:00:00 2001 From: Baldinof Date: Tue, 22 Mar 2016 15:34:35 +0100 Subject: [PATCH 080/618] 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 94789702d6..87657e4e3d 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 8d9908128e..691b706ea4 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 0000000000..d0703effa1 --- /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 1ca78daa5b..59c5ffa6b9 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 0000000000..f287b0a59b --- /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 0a14de843c9f47482521e3f703956ba2a6c89f1c Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 22 Mar 2016 11:45:23 -0500 Subject: [PATCH 081/618] Rename method for better understanding --- app/assets/javascripts/gl_crop.js.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index 137d025ed2..78dfe43d24 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -27,9 +27,9 @@ class GitLabCrop # Ensure needed elements are jquery objects # If selector is provided we will convert them to a jQuery Object - @filename = @$(@filename) - @previewImage = @$(@previewImage) - @pickImageEl = @$(@pickImageEl) + @filename = @getElement(@filename) + @previewImage = @getElement(@previewImage) + @pickImageEl = @getElement(@pickImageEl) # Modal elements usually are outside the @form element @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop @@ -40,7 +40,7 @@ class GitLabCrop @bindEvents() - $: (selector) -> + getElement: (selector) -> $(selector, @form) bindEvents: -> From f2ad7aabf90b4f5a97dd9dd329488f3aab7c6fcc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 22 Mar 2016 11:45:37 -0500 Subject: [PATCH 082/618] Use _this instead of self --- app/assets/javascripts/gl_crop.js.coffee | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index 78dfe43d24..3eb2eaa9a6 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -44,9 +44,9 @@ class GitLabCrop $(selector, @form) bindEvents: -> - self = @ + _this = @ @fileInput.on 'change', (e) -> - self.onFileInputChange(e, @) + _this.onFileInputChange(e, @) @pickImageEl.on 'click', @onPickImageClick @modalCrop.on 'shown.bs.modal', @onModalShow @@ -54,14 +54,14 @@ class GitLabCrop @uploadImageBtn.on 'click', @onUploadImageBtnClick @cropActionsBtn.on 'click', (e) -> btn = @ - self.onActionBtnClick(btn) + _this.onActionBtnClick(btn) @croppedImageBlob = null onPickImageClick: => @fileInput.trigger('click') onModalShow: => - self = @ + _this = @ @modalCropImg.cropper( viewMode: 1 center: false @@ -78,11 +78,12 @@ class GitLabCrop cropBoxResizable: false toggleDragModeOnDblclick: false built: -> - container = $(@).cropper 'getContainerData' - cropBoxWidth = self.cropBoxWidth; - cropBoxHeight = self.cropBoxHeight; + $image = $(@) + container = $image.cropper 'getContainerData' + cropBoxWidth = _this.cropBoxWidth; + cropBoxHeight = _this.cropBoxHeight; - $(@).cropper('setCropBoxData', + $image.cropper('setCropBoxData', width: cropBoxWidth, height: cropBoxHeight, left: (container.width - cropBoxWidth) / 2, @@ -113,11 +114,11 @@ class GitLabCrop @readFile(input) readFile: (input) -> - self = @ + _this = @ reader = new FileReader reader.onload = -> - self.modalCropImg.attr('src', reader.result) - self.modalCrop.modal('show') + _this.modalCropImg.attr('src', reader.result) + _this.modalCrop.modal('show') reader.readAsDataURL(input.files[0]) From 3ade8de55a3d83b8c4b5d3111251f29c3c465237 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 22 Mar 2016 12:37:54 -0500 Subject: [PATCH 083/618] Explain regular expression --- app/assets/javascripts/gl_crop.js.coffee | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index 3eb2eaa9a6..172f796a8d 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -1,4 +1,7 @@ class GitLabCrop + # Matches everything but the file name + FILENAMEREGEX = /^.*[\\\/]/ + constructor: (input, opts = {}) -> @fileInput = $(input) @@ -131,7 +134,7 @@ class GitLabCrop setPreview: -> @previewImage.attr('src', @dataURL) - filename = @fileInput.val().replace(/^.*[\\\/]/, '') + filename = @fileInput.val().replace(FILENAMEREGEX, '') @filename.text(filename) setBlob: -> From 10ffbb85592612a17b05ade7772efecf41c2fc1e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 22 Mar 2016 12:54:51 -0500 Subject: [PATCH 084/618] Add comment explaining why we enable form submit button --- app/assets/javascripts/profile.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee index 2fcc6dfd56..ae87c6c4e4 100644 --- a/app/assets/javascripts/profile.js.coffee +++ b/app/assets/javascripts/profile.js.coffee @@ -59,6 +59,7 @@ class @Profile new Flash(jqXHR.responseJSON.message, 'alert') complete: -> window.scrollTo 0, 0 + # Enable submit button after requests ends self.form.find(':input[disabled]').enable() $ -> From d5479eb06235559a3cee4b9da68952b87b0cc1bf Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 22 Mar 2016 12:55:42 -0500 Subject: [PATCH 085/618] Remove unnecessary line --- app/assets/javascripts/gl_crop.js.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index 172f796a8d..a9bee3cab7 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -110,7 +110,6 @@ class GitLabCrop data = $(btn).data() if @modalCropImg.data('cropper') && data.method - data = $.extend {}, data result = @modalCropImg.cropper data.method, data.option onFileInputChange: (e, input) -> From 444e92f40083319a1282b01a6736c140b600e404 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 22 Mar 2016 13:43:32 -0500 Subject: [PATCH 086/618] Clear original input after crop This fixes the case when the user selects the same image consecutively. It failed because no 'change' event was fired. --- app/assets/javascripts/gl_crop.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee index a9bee3cab7..df9bfdfa6c 100644 --- a/app/assets/javascripts/gl_crop.js.coffee +++ b/app/assets/javascripts/gl_crop.js.coffee @@ -105,6 +105,7 @@ class GitLabCrop @setBlob() @setPreview() @modalCrop.modal('hide') + @fileInput.val('') onActionBtnClick: (btn) -> data = $(btn).data() From b25d42cee9db265afe15d8ef192490a1ce2e3471 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Mon, 21 Mar 2016 21:25:15 -0500 Subject: [PATCH 087/618] 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 08d0a6a5bf..739b7e1019 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 0000000000..07e548aaab --- /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 0000000000..237700bbcd --- /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 cf1f98492e..fb20308c49 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 b1a93fffacdc0994021153aa88407c04054ae317 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Tue, 22 Mar 2016 22:54:34 +0100 Subject: [PATCH 088/618] fix missing tooltip in project title --- app/views/groups/show.html.haml | 2 +- app/views/projects/_home_panel.html.haml | 2 +- app/views/shared/groups/_group.html.haml | 2 +- app/views/shared/projects/_project.html.haml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 820743dc8d..3d16ecb097 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -17,7 +17,7 @@ .cover-title %h1 = @group.name - %span.visibility-icon.has_tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } + %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } = visibility_level_icon(@group.visibility_level, fw: false) .cover-desc.username diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 514cbfa339..9b5de17dd3 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -5,7 +5,7 @@ .cover-title.project-home-desc %h1 = @project.name - %span.visibility-icon.has_tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} + %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)} = visibility_level_icon(@project.visibility_level, fw: false) - if @project.description.present? diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 66b7ef9965..40c6eb9be4 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -21,7 +21,7 @@ = icon('users') = number_with_delimiter(group.users.count) - %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} = visibility_level_icon(group.visibility_level, fw: false) = image_tag group_icon(group), class: "avatar s40 hidden-xs" diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 803dd95bc6..53ff8959bc 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -27,7 +27,7 @@ %span = icon('star') = project.star_count - %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} = visibility_level_icon(project.visibility_level, fw: false) .title From f4a361a2a878086405240490567668dfac8d9bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Tue, 22 Mar 2016 18:01:31 -0500 Subject: [PATCH 089/618] Sanitize commit title when creating revert commit. --- app/models/commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index ce0b85d50c..d0dbe009d0 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -230,7 +230,7 @@ class Commit end def revert_message - %Q{Revert "#{title}"\n\n#{revert_description}} + %Q{Revert "#{title.strip}"\n\n#{revert_description}} end def reverts_commit?(commit) 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 090/618] 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 712e9fdf93..b9abcbd2c1 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 19a2adfa0cd42d48600bb3036c6ff90d15bb41e6 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 22 Mar 2016 20:26:57 -0400 Subject: [PATCH 091/618] Add special cases for built-in Rails routes in development --- config/routes.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index 90d858d7fc..a4b9987174 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,18 @@ Rails.application.routes.draw do end end + # Make the built-in Rails routes available in development, otherwise they'd + # get swallowed by the `namespace/project` route matcher below. + # + # See https://git.io/va79N + if Rails.env.development? + get '/rails/mailers' => 'rails/mailers#index' + get '/rails/mailers/:path' => 'rails/mailers#preview' + get '/rails/info/properties' => 'rails/info#properties' + get '/rails/info/routes' => 'rails/info#routes' + get '/rails/info' => 'rails/info#index' + end + namespace :ci do # CI API Ci::API::API.logger Rails.logger From 8d147219cdee7eb7f698f0330576f854e0bd0a04 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 18 Mar 2016 16:53:15 +0000 Subject: [PATCH 092/618] Added clear button to dropdown filter --- app/assets/javascripts/gl_dropdown.js.coffee | 21 +++++++++++++++++-- .../stylesheets/framework/dropdowns.scss | 20 ++++++++++++++++-- app/helpers/dropdowns_helper.rb | 3 ++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 960585245d..be05250a3a 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -1,13 +1,30 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] + HAS_VALUE_CLASS = "has-value" constructor: (@dropdown, @options) -> - @input = @dropdown.find(".dropdown-input .dropdown-input-field") + @input = @dropdown.find('.dropdown-input .dropdown-input-field') + $inputContainer = @input.parent() + $clearButton = $inputContainer.find('.js-dropdown-input-clear') + + # Clear click + $clearButton.on 'click', (e) => + e.preventDefault() + e.stopPropagation() + @input + .val('') + .trigger('keyup') + .focus() # Key events timeout = "" @input.on "keyup", (e) => - if e.keyCode is 13 && @input.val() isnt "" + if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS + $inputContainer.addClass HAS_VALUE_CLASS + else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS + $inputContainer.removeClass HAS_VALUE_CLASS + + if e.keyCode is 13 and @input.val() isnt "" if @options.enterCallback @options.enterCallback() return diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index d92cf6e6c4..fbead004ad 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -270,6 +270,22 @@ font-size: 12px; pointer-events: none; } + + .dropdown-input-clear { + display: none; + cursor: pointer; + pointer-events: all; + } + + &.has-value { + .dropdown-input-clear { + display: block; + } + + .dropdown-input-search { + display: none; + } + } } .dropdown-input-field { @@ -286,13 +302,13 @@ border-color: $dropdown-input-focus-border; box-shadow: 0 0 4px $dropdown-input-focus-shadow; - + .fa { + ~ .fa { color: $dropdown-link-color; } } &:hover { - + .fa { + ~ .fa { color: $dropdown-link-color; } } diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index ceff1fbb16..316a10b7da 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -70,7 +70,8 @@ module DropdownsHelper def dropdown_filter(placeholder) content_tag :div, class: "dropdown-input" do filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder - filter_output << icon('search') + filter_output << icon('search', class: "dropdown-input-search") + filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button") filter_output.html_safe end From 6d40eee5ea5b37689e97eb26900f06ba51cf5715 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 18 Mar 2016 17:11:51 +0000 Subject: [PATCH 093/618] Added label color box in dropdown --- app/assets/javascripts/labels_select.js.coffee | 3 +++ app/assets/stylesheets/framework/dropdowns.scss | 14 +++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index f3cb1e3bc0..684ad56cdb 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -78,8 +78,11 @@ class @LabelsSelect else selected = if label.title is selectedLabel then 'is-active' else '' + color = if label.color? then "" else "" + "
  • + #{color} #{label.title}
  • " diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index fbead004ad..0fb7d94d0d 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -354,11 +354,11 @@ } } -.dropdown-menu-labels { - .label { - position: relative; - width: 30px; - margin-right: 5px; - text-indent: -99999px; - } +.dropdown-label-box { + position: relative; + top: 3px; + display: inline-block; + width: 20px; + height: 16px; + border-radius: 3px; } From aec0e226691ef429a41adade45147ace61732514 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 22 Mar 2016 09:45:05 +0000 Subject: [PATCH 094/618] Adjusted alignment for the new label form --- .../javascripts/labels_select.js.coffee | 6 ++++ .../stylesheets/framework/dropdowns.scss | 11 ++++--- .../stylesheets/framework/variables.scss | 4 +-- app/assets/stylesheets/pages/labels.scss | 33 ++++++++++++++----- .../shared/issuable/_label_dropdown.html.haml | 14 +++++--- 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 684ad56cdb..7805e95f96 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -23,8 +23,14 @@ class @LabelsSelect newColorField.val $(this).data('color') $('.js-dropdown-label-color-preview') .css 'background-color', $(this).data('color') + .parent() .addClass 'is-active' + $('.js-cancel-label-btn').on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + $('.js-new-label-btn').on 'click', (e) -> e.preventDefault() e.stopPropagation() diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 0fb7d94d0d..20afc5c9dc 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -183,7 +183,7 @@ } .dropdown-select { - width: 280px; + width: 300px; } .dropdown-menu-align-right { @@ -237,7 +237,7 @@ .dropdown-title-button { position: absolute; - top: -1px; + top: 0; padding: 0; color: $dropdown-title-btn-color; font-size: 14px; @@ -357,8 +357,9 @@ .dropdown-label-box { position: relative; top: 3px; + margin-right: 5px; display: inline-block; - width: 20px; - height: 16px; - border-radius: 3px; + width: 15px; + height: 15px; + border-radius: $border-radius-base; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index be626678bd..b014031e75 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -168,13 +168,13 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif */ $dropdown-bg: #fff; $dropdown-link-color: #555; -$dropdown-link-hover-bg: rgba(#000, .04); +$dropdown-link-hover-bg: $row-hover; $dropdown-border-color: rgba(#000, .1); $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; $dropdown-title-btn-color: #bfbfbf; -$dropdown-input-color: #c7c7c7; +$dropdown-input-color: #555; $dropdown-input-focus-border: rgb(58, 171, 240); $dropdown-input-focus-shadow: rgba(#000, .2); $dropdown-loading-bg: rgba(#fff, .6); diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3c13573c8f..4e02ec4e89 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -9,28 +9,45 @@ } &.suggest-colors-dropdown { - margin-bottom: 5px; + margin-top: 10px; + margin-bottom: 10px; + border-radius: $border-radius-base; + overflow: hidden; a { @include border-radius(0); - width: 36.7px; + width: (100% / 7); margin-right: 0; margin-bottom: -5px; } } } -.dropdown-label-color-preview { - display: none; - margin-top: 5px; - width: 100%; - height: 25px; +.dropdown-new-label { + .dropdown-content { + max-height: 260px; + } +} + +.dropdown-label-color-input { + position: relative; + margin-bottom: 10px; &.is-active { - display: block; + padding-left: 32px; } } +.dropdown-label-color-preview { + position: absolute; + left: 0; + top: 0; + width: 32px; + height: 32px; + border-top-left-radius: $border-radius-base; + border-bottom-left-radius: $border-radius-base; +} + .label-row { .label { padding: 9px; diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 186087e8f8..006a34a11e 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -24,17 +24,21 @@ - else View labels - if can? current_user, :admin_label, @project and @project - .dropdown-page-two + .dropdown-page-two.dropdown-new-label = dropdown_title("Create new label", back: true) = dropdown_content do .dropdown-labels-error.js-label-error - %input#new_label_color{type: "hidden"} %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} - .dropdown-label-color-preview.js-dropdown-label-color-preview .suggest-colors.suggest-colors-dropdown - suggested_colors.each do |color| = link_to '#', style: "background-color: #{color}", data: { color: color } do   - %button.btn.btn-primary.js-new-label-btn{type: "button"} - Create + .dropdown-label-color-input + .dropdown-label-color-preview.js-dropdown-label-color-preview + %input#new_label_color.dropdown-input-field{ type: "text" } + .clearfix + %button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"} + Create + %button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"} + Cancel = dropdown_loading From 4c5babd94bce061a77073025fe5bf63433624ea8 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 22 Mar 2016 09:49:14 +0000 Subject: [PATCH 095/618] Removed bold on any/unassigned authors --- app/assets/javascripts/users_select.js.coffee | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 3d6452d2f4..8419340089 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -30,6 +30,7 @@ class @UsersSelect if showNullUser showDivider += 1 users.unshift( + beforeDivider: true name: 'Unassigned', id: 0 ) @@ -39,6 +40,7 @@ class @UsersSelect name = showAnyUser name = 'Any User' if name == true anyUser = { + beforeDivider: true name: name, id: null } @@ -75,20 +77,27 @@ class @UsersSelect selected = if user.id is selectedId then "is-active" else "" img = "" - if avatar - img = "" - - "
  • - - #{img} - + if user.beforeDivider? + "
  • + #{user.name} - - - #{username} - - -
  • " + + " + else + if avatar + img = "" + + "
  • + + #{img} + + #{user.name} + + + #{username} + + +
  • " ) $('.ajax-users-select').each (i, select) => From fee01133eafdb46972a22d9806898e79e7245ef2 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 22 Mar 2016 10:17:03 +0000 Subject: [PATCH 096/618] Changed colour of no matching results bg Highlights first row when filtering --- app/assets/javascripts/gl_dropdown.js.coffee | 20 ++++++++++++++----- .../stylesheets/framework/dropdowns.scss | 6 ++++++ .../stylesheets/framework/variables.scss | 1 + 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index be05250a3a..4b78bcde77 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -2,8 +2,7 @@ class GitLabDropdownFilter BLUR_KEYCODES = [27, 40] HAS_VALUE_CLASS = "has-value" - constructor: (@dropdown, @options) -> - @input = @dropdown.find('.dropdown-input .dropdown-input-field') + constructor: (@input, @options) -> $inputContainer = @input.parent() $clearButton = $inputContainer.find('.js-dropdown-input-clear') @@ -112,7 +111,9 @@ class GitLabDropdown # Init filiterable if @options.filterable - @filter = new GitLabDropdownFilter @dropdown, + @input = @dropdown.find('.dropdown-input .dropdown-input-field') + + @filter = new GitLabDropdownFilter @input, remote: @options.filterRemote query: @options.data keys: @options.search.fields @@ -120,6 +121,7 @@ class GitLabDropdown return @fullData callback: (data) => @parseData data + @highlightRow 1 enterCallback: => @selectFirstRow() @@ -241,11 +243,19 @@ class GitLabDropdown noResults: -> html = "
  • " - html += "" + html += "" html += "No matching results." html += "" html += "
  • " + highlightRow: (index) -> + if @input.val() isnt "" + selector = '.dropdown-content li:first-child a' + if @dropdown.find(".dropdown-toggle-page").length + selector = ".dropdown-page-one .dropdown-content li:first-child a" + + $(selector).addClass 'is-focused' + rowClicked: (el) -> fieldName = @options.fieldName field = @dropdown.parent().find("input[name='#{fieldName}']") @@ -289,7 +299,7 @@ class GitLabDropdown if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one .dropdown-content li:first-child a" - # similute a click on the first link + # simulate a click on the first link $(selector).trigger "click" $.fn.glDropdown = (opts) -> diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 20afc5c9dc..2d616fc660 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -130,6 +130,12 @@ text-decoration: none; outline: 0; } + + &.dropdown-menu-empty-link { + &.is-focused { + background-color: $dropdown-empty-row-bg; + } + } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b014031e75..61e0dd4d67 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -169,6 +169,7 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif $dropdown-bg: #fff; $dropdown-link-color: #555; $dropdown-link-hover-bg: $row-hover; +$dropdown-empty-row-bg: rgba(#000, .04); $dropdown-border-color: rgba(#000, .1); $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); From bb0a4c057f25dcdba41be36b96faa808ac80c852 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 22 Mar 2016 13:23:01 +0000 Subject: [PATCH 097/618] Fixed failing tests --- features/steps/dashboard/issues.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index f4a5686553..93aa77589b 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -43,10 +43,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps step 'I click "All" link' do find('.js-author-search').click - find('.dropdown-menu-user-full-name', match: :first).click + find('.dropdown-content a', match: :first).click find('.js-assignee-search').click - find('.dropdown-menu-user-full-name', match: :first).click + find('.dropdown-content a', match: :first).click end def should_see(issue) From 7e2806a1230f57b73e7d0628ba917af96537f8fc Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 23 Mar 2016 10:36:30 +0000 Subject: [PATCH 098/618] Added tests for setting multiple status on issues --- app/views/shared/issuable/_filter.html.haml | 2 +- spec/features/issues/update_issues_spec.rb | 47 +++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 spec/features/issues/update_issues_spec.rb diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 53952e608e..781d890b51 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -31,7 +31,7 @@ .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do .filter-item.inline - = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do %ul %li %a{href: "#", data: {id: "reopen"}} Open diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb new file mode 100644 index 0000000000..27707d49b9 --- /dev/null +++ b/spec/features/issues/update_issues_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +feature 'Multiple issue updating from issues#index', feature: true do + let!(:project) { create(:project) } + let!(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + context 'status update', js: true do + before do + project.team << [user, :master] + login_as(user) + end + + it 'should be set to closed' do + visit namespace_project_issues_path(project.namespace, project) + + find('#check_all_issues').click + find('.js-issue-status').click + + find('.dropdown-menu-status a', text: 'Closed').click + click_update_issues_button + expect(page).to have_selector('.issue', count: 0) + end + + it 'should be set to open' do + create_closed + visit namespace_project_issues_path(project.namespace, project) + + find('.issues-state-filters a', text: 'Closed').click + + find('#check_all_issues').click + find('.js-issue-status').click + + find('.dropdown-menu-status a', text: 'Open').click + click_update_issues_button + expect(page).to have_selector('.issue', count: 0) + end + end + + def create_closed + create(:issue, project: project, state: :closed) + end + + def click_update_issues_button + find('.update_selected_issues').click + end +end From 84f124a14fe9f955f737386b2805d1203e28259e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 23 Mar 2016 11:18:39 +0000 Subject: [PATCH 099/618] Assignee tests --- app/views/shared/issuable/_filter.html.haml | 2 +- spec/features/issues/update_issues_spec.rb | 40 +++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 781d890b51..719ddfa988 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -38,7 +38,7 @@ %li %a{href: "#", data: {id: "close"}} Closed .filter-item.inline - = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 27707d49b9..70e218cc14 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -37,10 +37,50 @@ feature 'Multiple issue updating from issues#index', feature: true do end end + context 'assignee update', js: true do + before do + project.team << [user, :master] + login_as(user) + end + + it 'should update to current user' do + visit namespace_project_issues_path(project.namespace, project) + + find('#check_all_issues').click + find('.js-update-assignee').click + + find('.dropdown-menu-user-link', text: user.username).click + click_update_issues_button + + page.within('.issue .controls') do + expect(find('.author_link')["data-original-title"]).to have_content(user.name) + end + end + + it 'should update to unassigned' do + create_assigned + visit namespace_project_issues_path(project.namespace, project) + + find('#check_all_issues').click + find('.js-update-assignee').click + + find('.dropdown-menu-user-link', text: "Unassigned").click + click_update_issues_button + + within first('.issue .controls') do + expect(page).to have_no_selector('.author_link') + end + end + end + def create_closed create(:issue, project: project, state: :closed) end + def create_assigned + create(:issue, project: project, assignee: user) + end + def click_update_issues_button find('.update_selected_issues').click end From 23bfa7a3bdee0ba192babc274bb14b6a51dc8209 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 23 Mar 2016 11:45:24 +0000 Subject: [PATCH 100/618] Update milestone spec --- app/views/shared/issuable/_filter.html.haml | 2 +- spec/features/issues/update_issues_spec.rb | 52 ++++++++++++++++----- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 719ddfa988..f91ff0e369 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -41,7 +41,7 @@ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) .filter-item.inline - = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable", + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 70e218cc14..121954fabc 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -5,12 +5,12 @@ feature 'Multiple issue updating from issues#index', feature: true do let!(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} - context 'status update', js: true do - before do - project.team << [user, :master] - login_as(user) - end + before do + project.team << [user, :master] + login_as(user) + end + context 'status', js: true do it 'should be set to closed' do visit namespace_project_issues_path(project.namespace, project) @@ -37,12 +37,7 @@ feature 'Multiple issue updating from issues#index', feature: true do end end - context 'assignee update', js: true do - before do - project.team << [user, :master] - login_as(user) - end - + context 'assignee', js: true do it 'should update to current user' do visit namespace_project_issues_path(project.namespace, project) @@ -73,6 +68,37 @@ feature 'Multiple issue updating from issues#index', feature: true do end end + context 'milestone', js: true do + let(:milestone) { create(:milestone, project: project) } + + it 'should update milestone' do + visit namespace_project_issues_path(project.namespace, project) + + find('#check_all_issues').click + find('.issues_bulk_update .js-milestone-select').click + + find('.dropdown-menu-milestone a', text: milestone.title).click + click_update_issues_button + + expect(find('.issue')).to have_content milestone.title + end + + it 'should set to no milestone' do + create_with_milestone + visit namespace_project_issues_path(project.namespace, project) + + expect(first('.issue')).to have_content milestone.title + + find('#check_all_issues').click + find('.issues_bulk_update .js-milestone-select').click + + find('.dropdown-menu-milestone a', text: "No Milestone").click + click_update_issues_button + + expect(first('.issue')).to_not have_content milestone.title + end + end + def create_closed create(:issue, project: project, state: :closed) end @@ -81,6 +107,10 @@ feature 'Multiple issue updating from issues#index', feature: true do create(:issue, project: project, assignee: user) end + def create_with_milestone + create(:issue, project: project, milestone: milestone) + end + def click_update_issues_button find('.update_selected_issues').click end From 7eaad6e46a42c7c118706bdf8676edd48275b642 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 23 Mar 2016 12:02:07 +0000 Subject: [PATCH 101/618] Fixed bold in sidebar --- app/views/shared/issuable/_sidebar.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 5b2772de3f..4789ae98f7 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -77,7 +77,7 @@ Labels - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.issuable-show-labels.hide-collapsed{class: ("has-labels" if issuable.labels.any?)} + .value.bold.issuable-show-labels.hide-collapsed{class: ("has-labels" if issuable.labels.any?)} - if issuable.labels.any? - issuable.labels.each do |label| = link_to_label(label, type: issuable.to_ability_name) From 9374b7eb0b28527123abad40a070ee4a5b1f0d9f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 23 Mar 2016 20:05:31 +0800 Subject: [PATCH 102/618] Avoid using the same name between methods and variables --- lib/gitlab/email/receiver.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index 2ca21af5bc..d4b6f6d120 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -45,12 +45,12 @@ module Gitlab note = create_note(reply) unless note.persisted? - message = "The comment could not be created for the following reasons:" + msg = "The comment could not be created for the following reasons:" note.errors.full_messages.each do |error| - message << "\n\n- #{error}" + msg << "\n\n- #{error}" end - raise InvalidNoteError, message + raise InvalidNoteError, msg end end @@ -63,13 +63,13 @@ module Gitlab end def reply_key - reply_key = nil + key = nil message.to.each do |address| - reply_key = Gitlab::IncomingEmail.key_from_address(address) - break if reply_key + key = Gitlab::IncomingEmail.key_from_address(address) + break if key end - reply_key + key end def sent_notification From ecc060d701968b75009f47a3b7ac5b727022782c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 23 Mar 2016 20:22:09 +0800 Subject: [PATCH 103/618] Make sure we get only two returns --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 9c8246e8ac..2285063ab5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -304,7 +304,7 @@ class Project < ActiveRecord::Base end def find_with_namespace(id) - namespace_path, project_path = id.split('/') + namespace_path, project_path = id.split('/', 2) return nil if !namespace_path || !project_path From bab50e0133e226c06b7a968c2bfa46717de81485 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 23 Mar 2016 12:41:53 +0100 Subject: [PATCH 104/618] Preserve time notes has been updated at when moving issue --- app/services/issues/move_service.rb | 3 ++- spec/services/issues/move_service_spec.rb | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 468f8acdf6..a5efb21fab 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -54,7 +54,8 @@ module Issues new_note = note.dup new_params = { project: @new_project, noteable: @new_issue, note: unfold_references(new_note.note), - created_at: note.created_at } + created_at: note.created_at, + updated_at: note.updated_at } new_note.update(new_params) end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index ade3b7850f..9b0c73aaf3 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -85,6 +85,10 @@ describe Issues::MoveService, services: true do expect(old_issue.moved?).to eq true expect(old_issue.moved_to).to eq new_issue end + + it 'preserves create time' do + expect(old_issue.created_at).to eq new_issue.created_at + end end context 'issue with notes' do @@ -121,10 +125,23 @@ describe Issues::MoveService, services: true do it 'preserves orignal author of comment' do expect(user_notes.pluck(:author_id)).to all(eq(author.id)) end + end + + context 'note that has been updated' do + let!(:note) do + create(:note, noteable: old_issue, project: old_project, + author: author, updated_at: Date.yesterday, + created_at: Date.yesterday) + end + + include_context 'issue move executed' it 'preserves time when note has been created at' do - expect(old_issue.notes.first.created_at) - .to eq new_issue.notes.first.created_at + expect(new_issue.notes.first.created_at).to eq note.created_at + end + + it 'preserves time when note has been updated at' do + expect(new_issue.notes.first.updated_at).to eq note.updated_at end end From 85b9d763f17e4f8a24610684b570ae02bf0f7522 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 23 Mar 2016 13:24:15 +0100 Subject: [PATCH 105/618] Add Changelog entry for preserving timestamps when moving issue --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 924358a4f0..79e49d4190 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) v 8.6.1 (unreleased) From 0141d3654ffd535366a4405c0ee58f9a6998c58f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 23 Mar 2016 13:29:41 +0000 Subject: [PATCH 106/618] Disable new label form in dropdown until fields are complete --- .../javascripts/labels_select.js.coffee | 79 ++++++++++++++----- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 7805e95f96..e08648d583 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -6,7 +6,7 @@ class @LabelsSelect labelUrl = $dropdown.data('labels') selectedLabel = $dropdown.data('selected') if selectedLabel - selectedLabel = selectedLabel.split(',') + selectedLabel = selectedLabel.toString().split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') showNo = $dropdown.data('show-no') @@ -14,44 +14,81 @@ class @LabelsSelect defaultLabel = $dropdown.data('default-label') if newLabelField.length + $newLabelCreateButton = $('.js-new-label-btn') + $colorPreview = $('.js-dropdown-label-color-preview') $newLabelError = $dropdown.parent().find('.js-label-error') $newLabelError.hide() + # Suggested colors in the dropdown to chose from pre-chosen colors $('.suggest-colors-dropdown a').on 'click', (e) -> e.preventDefault() e.stopPropagation() - newColorField.val $(this).data('color') - $('.js-dropdown-label-color-preview') + newColorField + .val($(this).data('color')) + .trigger('change') + $colorPreview .css 'background-color', $(this).data('color') .parent() .addClass 'is-active' + # Cancel button takes back to first page + resetForm = -> + newLabelField + .val '' + .trigger 'change' + newColorField + .val '' + .trigger 'change' + $colorPreview + .css 'background-color', '' + .parent() + .removeClass 'is-active' + + $('.dropdown-menu-back').on 'click', -> + resetForm() + $('.js-cancel-label-btn').on 'click', (e) -> e.preventDefault() e.stopPropagation() + resetForm() $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' - $('.js-new-label-btn').on 'click', (e) -> - e.preventDefault() - e.stopPropagation() - + # Listen for change and keyup events on label and color field + # This allows us to enable the button when ready + enableLabelCreateButton = -> if newLabelField.val() isnt '' and newColorField.val() isnt '' - $newLabelError.hide() - $('.js-new-label-btn').disable() + $newLabelCreateButton.enable() + else + $newLabelCreateButton.disable() - # Create new label with API - Api.newLabel projectId, { - name: newLabelField.val() - color: newColorField.val() - }, (label) -> - $('.js-new-label-btn').enable() + newLabelField.on 'keyup change', enableLabelCreateButton - if label.message? - $newLabelError - .text label.message - .show() - else - $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' + newColorField.on 'keyup change', enableLabelCreateButton + + # Send the API call to create the label + $newLabelCreateButton + .disable() + .on 'click', (e) -> + e.preventDefault() + e.stopPropagation() + + if newLabelField.val() isnt '' and newColorField.val() isnt '' + $newLabelError.hide() + $('.js-new-label-btn').disable() + + # Create new label with API + Api.newLabel projectId, { + name: newLabelField.val() + color: newColorField.val() + }, (label) -> + $('.js-new-label-btn').enable() + + if label.message? + $newLabelError + .text label.message + .show() + else + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' $dropdown.glDropdown( data: (term, callback) -> From c23d9ab4edfd0b9f5d28e81759d48862e3e06b15 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 23 Mar 2016 13:59:54 +0000 Subject: [PATCH 107/618] Fixed error with applications delete enonymous token form Closes #14509 --- app/views/doorkeeper/applications/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index ea0b66c932..55f4a6f287 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -77,7 +77,7 @@ %em Authorization was granted by entering your username and password in the application. %td= token.created_at %td= token.scopes - %td= render 'delete_form', token: token + %td= render 'doorkeeper/authorized_applications/delete_form', token: token - else .profile-settings-message.text-center You don't have any authorized applications From a4fa5d3e3d0ab680495c8cfbe9fb85fd25dfe767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 23 Mar 2016 15:58:02 +0100 Subject: [PATCH 108/618] Updated 8.6.1 changelog [ci skip] --- CHANGELOG | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4088057c4e..2c1f2a5400 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,10 +4,20 @@ v 8.7.0 (unreleased) - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) -v 8.6.1 (unreleased) - - Do not allow to move issue if it has not been persisted - - Fix an issue causing the Dashboard/Milestones page to be blank - - Fix build dependencies, when the dependency is a string +v 8.6.1 + - Add option to reload the schema before restoring a database backup. !2807 + - Display navigation controls on mobile. !3214 + - Fixed bug where participants would not work correctly on merge requests. !3329 + - Fix sorting issues by votes on the groups issues page results in SQL errors. !3333 + - Restrict notifications for confidential issues. !3334 + - Do not allow to move issue if it has not been persisted. !3340 + - Add a confirmation step before deleting an issuable. !3341 + - Fixes issue with signin button overflowing on mobile. !3342 + - Auto collapses the navigation sidebar when resizing. !3343 + - Fix build dependencies, when the dependency is a string. !3344 + - Shows error messages when trying to create label in dropdown menu. !3345 + - Fixes issue with assign milestone not loading milestone list. !3346 + - Fix an issue causing the Dashboard/Milestones page to be blank. !3348 v 8.6.0 - Add ability to move issue to another project @@ -34,7 +44,6 @@ v 8.6.0 - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner) - HTTP error pages work independently from location and config (Artem Sidorenko) - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - - Add option to reload the schema before restoring a database backup. !2807 - Memoize @group in Admin::GroupsController (Yatish Mehta) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Added omniauth-auth0 Gem (Daniel Carraro) @@ -73,7 +82,6 @@ v 8.6.0 - Canceled builds are now ignored in compound build status if marked as `allowed to fail` - Trigger a todo for mentions on commits page - Let project owners and admins soft delete issues and merge requests - - Fix sorting issues by votes on the groups issues page results in SQL errors v 8.5.8 - Bump Git version requirement to 2.7.4 From b651a4fd410749642af7a0b658ef2e60a3f45ac9 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 23 Mar 2016 15:09:58 +0000 Subject: [PATCH 109/618] Fix Milestone.upcoming --- app/models/milestone.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index de7183bf6b..bbd59eab9a 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -83,7 +83,7 @@ class Milestone < ActiveRecord::Base end def self.upcoming - self.where('due_date > ?', Time.now).order(due_date: :asc).first + self.where('due_date > ?', Time.now).reorder(due_date: :asc).first end def to_reference(from_project = nil) From 3bab5976c087df2cbc9c5ac7552c46f97bab1322 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 23 Mar 2016 11:54:04 -0500 Subject: [PATCH 110/618] Move cropper.js to vendor folder --- app/assets/javascripts/application.js.coffee | 1 + .../javascripts/lib => vendor/assets/javascripts}/cropper.js | 0 2 files changed, 1 insertion(+) rename {app/assets/javascripts/lib => vendor/assets/javascripts}/cropper.js (100%) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 0145183065..293e0c3bb3 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -43,6 +43,7 @@ #= require jquery.nicescroll #= require_tree . #= require fuzzaldrin-plus +#= require cropper window.slugify = (text) -> text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase() diff --git a/app/assets/javascripts/lib/cropper.js b/vendor/assets/javascripts/cropper.js similarity index 100% rename from app/assets/javascripts/lib/cropper.js rename to vendor/assets/javascripts/cropper.js From 128c109a344a9316dccf736a03446246fcee0b5a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 23 Mar 2016 11:56:14 -0500 Subject: [PATCH 111/618] Move cropper.css to vendor folder --- {app => vendor}/assets/stylesheets/cropper.css | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {app => vendor}/assets/stylesheets/cropper.css (100%) diff --git a/app/assets/stylesheets/cropper.css b/vendor/assets/stylesheets/cropper.css similarity index 100% rename from app/assets/stylesheets/cropper.css rename to vendor/assets/stylesheets/cropper.css From e94482d1497cf6e1922261be48e5ebdcc33f44ae Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 18 Mar 2016 13:11:35 +0000 Subject: [PATCH 112/618] Issue sidebar overlaps on tablet --- app/assets/stylesheets/framework/sidebar.scss | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 6107c8a6d0..769477ea4a 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -27,14 +27,15 @@ } &.right-sidebar-expanded { - /* Extra small devices (phones, less than 768px) */ - /* No media query since this is the default in Bootstrap */ padding-right: 0; - /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { - padding-right: $gutter_width; + + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + padding-right: $sidebar_collapsed_width; } + @media (min-width: $screen-md-min) { + padding-right: $gutter_width; + } } } @@ -209,15 +210,6 @@ padding-left: $sidebar_width; } - &.right-sidebar-collapsed { - /* Extra small devices (phones, less than 768px) */ - padding-right: 0; - /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { - padding-right: $sidebar_collapsed_width; - } - } - .sidebar-wrapper { width: $sidebar_width; @@ -241,9 +233,8 @@ padding-left: $sidebar_collapsed_width; &.right-sidebar-collapsed { - /* Extra small devices (phones, less than 768px) */ padding-right: 0; - /* Small devices (tablets, 768px and up) */ + @media (min-width: $screen-sm-min) { padding-right: $sidebar_collapsed_width; } From 2d7183bbe8de88d0ef5156a6224ab71eeb6e2f8c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 21 Mar 2016 11:09:55 +0000 Subject: [PATCH 113/618] Fixed failing tests Moved the scss out of a mixin as it was really confusing the hell out of the CSS --- app/assets/javascripts/sidebar.js.coffee | 1 - app/assets/stylesheets/framework/sidebar.scss | 142 ++++++++---------- 2 files changed, 64 insertions(+), 79 deletions(-) diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index eea3f5ee91..860d4f438d 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded' toggleSidebar = -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") - $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded") $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 769477ea4a..1f5c15abaa 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -1,3 +1,10 @@ +#logo { + z-index: 2; + position: absolute; + width: 58px; + cursor: pointer; +} + .page-with-sidebar { padding-top: $header-height; transition-duration: .3s; @@ -18,25 +25,6 @@ position: absolute; left: 0; } - - #logo { - z-index: 2; - position: absolute; - width: 58px; - cursor: pointer; - } - - &.right-sidebar-expanded { - padding-right: 0; - - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - padding-right: $sidebar_collapsed_width; - } - - @media (min-width: $screen-md-min) { - padding-right: $gutter_width; - } - } } .sidebar-wrapper { @@ -203,43 +191,28 @@ } } -@mixin expanded-sidebar { - padding-left: $sidebar_collapsed_width; - - @media (min-width: $screen-md-min) { - padding-left: $sidebar_width; - } - - .sidebar-wrapper { - width: $sidebar_width; - - .nav-sidebar { - width: $sidebar_width; - } - - .nav-sidebar li a{ - width: 230px; - - &.back-link { - i { - opacity: 0; - } - } - } - } +.collapse-nav a { + width: $sidebar_width; + position: fixed; + bottom: 0; + left: 0; + font-size: 13px; + background: transparent; + height: 40px; + text-align: center; + line-height: 40px; + transition-duration: .3s; + outline: none; } -@mixin collapsed-sidebar { +.collapse-nav a:hover { + text-decoration: none; + background: #f2f6f7; +} + +.page-sidebar-collapsed { padding-left: $sidebar_collapsed_width; - &.right-sidebar-collapsed { - padding-right: 0; - - @media (min-width: $screen-sm-min) { - padding-right: $sidebar_collapsed_width; - } - } - .sidebar-wrapper { width: $sidebar_collapsed_width; @@ -284,35 +257,48 @@ } } -.collapse-nav a { - width: $sidebar_width; - position: fixed; - bottom: 0; - left: 0; - font-size: 13px; - background: transparent; - height: 40px; - text-align: center; - line-height: 40px; - transition-duration: .3s; - outline: none; -} +.page-sidebar-expanded { + padding-left: $sidebar_collapsed_width; -.collapse-nav a:hover { - text-decoration: none; - background: #f2f6f7; -} + @media (min-width: $screen-md-min) { + padding-left: $sidebar_width; + } -.page-sidebar-collapsed { - /* Extra small devices (phones, less than 768px) */ - @include collapsed-sidebar; - padding-right: 0; - /* Small devices (tablets, 768px and up) */ - @media (min-width: $screen-sm-min) { - @include collapsed-sidebar; + .sidebar-wrapper { + width: $sidebar_width; + + .nav-sidebar { + width: $sidebar_width; + } + + .nav-sidebar li a { + width: 230px; + + &.back-link { + i { + opacity: 0; + } + } + } } } -.page-sidebar-expanded { - @include expanded-sidebar; +.right-sidebar-collapsed { + padding-right: 0; + + @media (min-width: $screen-sm-min) { + padding-right: $sidebar_collapsed_width; + } +} + +.right-sidebar-expanded { + padding-right: 0; + + @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { + padding-right: $sidebar_collapsed_width; + } + + @media (min-width: $screen-md-min) { + padding-right: $gutter_width; + } } From 07a1da7e40d5ff7cd6d45463c2187d7f9c1460a8 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 23 Mar 2016 17:21:02 +0000 Subject: [PATCH 114/618] Removed hover background that wasn't needed --- app/assets/stylesheets/framework/sidebar.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 1f5c15abaa..9d18831778 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -203,11 +203,10 @@ line-height: 40px; transition-duration: .3s; outline: none; -} -.collapse-nav a:hover { - text-decoration: none; - background: #f2f6f7; + &:hover { + text-decoration: none; + } } .page-sidebar-collapsed { From 6c20e9a5bb6c6859081f550290e7b560cee6539a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 23 Mar 2016 13:34:24 -0500 Subject: [PATCH 115/618] Update CHANGELOG --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 9a102b8f7e..265302b135 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) - Make HTTP(s) label consistent on clone bar (Stan Hu) + - Fix avatar stretching by providing a cropping feature v 8.6.1 (unreleased) From 44817726fe52a5a061396a2280f7fd19c7d494d0 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 23 Mar 2016 14:12:33 -0500 Subject: [PATCH 116/618] 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 a8ae261c4d..a06c80b60c 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 0a5c145029..a778336581 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 117/618] 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 a06c80b60c..4aa658735d 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 2c9f6cfa4284a584a87d80fea53517f4a46bfabe Mon Sep 17 00:00:00 2001 From: connorshea Date: Wed, 23 Mar 2016 14:35:06 -0600 Subject: [PATCH 118/618] SCSS Style Guide fixes. Fix a typo and add a section on ignoring issues. [ci skip] --- doc/development/scss_styleguide.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/development/scss_styleguide.md b/doc/development/scss_styleguide.md index 6c48c25448..a79f4073cd 100644 --- a/doc/development/scss_styleguide.md +++ b/doc/development/scss_styleguide.md @@ -72,9 +72,9 @@ p { margin: 0; padding: 0; } ### Colors -HEX (hexadecimal) colors short-form should use shortform where possible, and -should use lower case letters to differenciate between letters and numbers, e. -g. `#E3E3E3` vs. `#e3e3e3`. +HEX (hexadecimal) colors should use shorthand where possible, and should use +lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3` +vs. `#e3e3e3`. ```scss // Bad @@ -160,6 +160,7 @@ is slightly more performant. ``` ### Selectors with a `js-` Prefix + Do not use any selector prefixed with `js-` for styling purposes. These selectors are intended for use only with JavaScript to allow for removal or renaming without breaking styling. @@ -187,8 +188,28 @@ CSSComb globally (system-wide). Run it in the GitLab directory with Note that this won't fix every problem, but it should fix a majority. +### Ignoring issues + +If you want a line or set of lines to be ignored by the linter, you can use +`// scss-lint:disable RuleName` ([more info][disabling-linters]): + +```scss +// This lint rule is disabled because the class name comes from a gem. +// scss-lint:disable SelectorFormat +.ui_charcoal { + background-color: #333; +} +// scss-lint:enable SelectorFormat +``` + +Make sure a comment is added on the line above the `disable` rule, otherwise the +linter will throw a warning. `DisableLinterReason` is enabled to make sure the +style guide isn't being ignored, and to communicate to others why the style +guide is ignored in this instance. + [csscomb]: https://github.com/csscomb/csscomb.js [node]: https://github.com/nodejs/node [npm]: https://www.npmjs.com/ [scss-lint]: https://github.com/brigade/scss-lint [scss-lint-documentation]: https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md +[disabling-linters]: https://github.com/brigade/scss-lint#disabling-linters-via-source From 30eeb453bd4ed0d710ae74a8bf5b8c8a48a2f96b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 23 Mar 2016 16:13:46 -0500 Subject: [PATCH 119/618] 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 4aa658735d..a7130c5796 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 = "" @dropdownContent.html(html) + + onClick: (item, e) -> + if location.pathname.indexOf(item.url) isnt -1 + e.preventDefault() + @disableAutocomplete() + @searchInput.val('') From 340f1fc97652a9c7c39c5d124e5726f537780f3f Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 30 Mar 2016 20:12:02 +0200 Subject: [PATCH 329/618] Minor clean up on admin/users_controller_spec --- spec/controllers/admin/users_controller_spec.rb | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 5b1f65d7af..9ef8ba1b09 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -1,15 +1,14 @@ require 'spec_helper' describe Admin::UsersController do - let(:admin) { create(:admin) } + let(:user) { create(:user) } before do - sign_in(admin) + sign_in(create(:admin)) end describe 'DELETE #user with projects' do - let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { create(:empty_project, namespace: user.namespace) } before do project.team << [user, :developer] @@ -23,8 +22,6 @@ describe Admin::UsersController do end describe 'PUT block/:id' do - let(:user) { create(:user) } - it 'blocks user' do put :block, id: user.username user.reload @@ -50,8 +47,6 @@ describe Admin::UsersController do end context 'manually blocked users' do - let(:user) { create(:user) } - before do user.block end @@ -66,8 +61,6 @@ describe Admin::UsersController do end describe 'PUT unlock/:id' do - let(:user) { create(:user) } - before do request.env["HTTP_REFERER"] = "/" user.lock_access! @@ -95,8 +88,6 @@ describe Admin::UsersController do end describe 'PATCH disable_two_factor' do - let(:user) { create(:user) } - it 'disables 2FA for the user' do expect(user).to receive(:disable_two_factor!) allow(subject).to receive(:user).and_return(user) From e67ce6cd331f0ea8519e07711aae09ae6f6c5326 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Fri, 1 Apr 2016 00:02:50 +0100 Subject: [PATCH 330/618] fix divider in dropdown when not needed --- app/views/projects/buttons/_dropdown.html.haml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index e7c85edff9..1e4c46fca2 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -3,25 +3,32 @@ %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} = icon('plus') %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - - if can?(current_user, :create_issue, @project) + - can_create_issue = can?(current_user, :create_issue, @project) + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - can_create_snippet = can?(current_user, :create_snippet, @project) + + - if can_create_issue %li = link_to url_for_new_issue(@project, only_path: true) do = icon('exclamation-circle fw') New issue - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project %li = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do = icon('tasks fw') New merge request - - if can?(current_user, :create_snippet, @project) + + - if can_create_snippet %li = link_to new_namespace_project_snippet_path(@project.namespace, @project) do = icon('file-text-o fw') New snippet - - if can?(current_user, :push_code, @project) + - if can_create_issue || merge_project || can_create_snippet %li.divider + + - if can?(current_user, :push_code, @project) %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') @@ -35,13 +42,11 @@ = icon('tags fw') New tag - elsif current_user && current_user.already_forked?(@project) - %li.divider %li = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do = icon('file fw') New file - elsif can?(current_user, :fork_project, @project) - %li.divider %li - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'), notice: edit_in_new_fork_notice, From 932c2f59cbf8c7a393b5e23fc8b79ce9a7db76e4 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Tue, 29 Mar 2016 19:05:02 +0100 Subject: [PATCH 331/618] hide user profile activity graph on mobile and enable horizontal scroll for medium screens --- app/assets/stylesheets/framework/calendar.scss | 6 ++++++ app/views/users/show.html.haml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index e3192823a1..0b3af592d4 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,3 +1,9 @@ +.calender-block { + @media (min-width: $screen-sm-min) and (max-width: $screen-lg-min) { + overflow-x: scroll; + } +} + .user-calendar-activities { .calendar_onclick_hr { padding: 0; diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index bca816f22c..0c4b6a5618 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -87,7 +87,7 @@ %div{ class: container_class } .tab-content #activity.tab-pane - .gray-content-block.white.second-block + .gray-content-block.calender-block.white.second-block.hidden-xs %div{ class: container_class } .user-calendar{data: {href: user_calendar_path}} %h4.center.light From 85cc1729596ac1e5b31d8cfa1daa07477db6033d Mon Sep 17 00:00:00 2001 From: connorshea Date: Thu, 31 Mar 2016 16:40:39 -0600 Subject: [PATCH 332/618] Remove "Congratulations!" tweet button on newly-created project. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’ve removed everything related to the feature based on this commit: ce08f919f34fd8849834365 Resolves #10857. --- CHANGELOG | 1 + app/assets/stylesheets/pages/events.scss | 4 ---- .../admin/application_settings_controller.rb | 1 - app/helpers/application_settings_helper.rb | 4 ---- app/models/application_setting.rb | 2 -- .../admin/application_settings/_form.html.haml | 7 ------- .../events/event/_created_project.html.haml | 18 ------------------ config/initializers/1_settings.rb | 1 - ...haring_enabled_from_application_settings.rb | 5 +++++ doc/api/settings.md | 3 --- lib/api/entities.rb | 1 - lib/gitlab/current_settings.rb | 1 - spec/models/application_setting_spec.rb | 1 - 13 files changed, 6 insertions(+), 43 deletions(-) create mode 100644 db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb diff --git a/CHANGELOG b/CHANGELOG index 5f73570650..47aa775cde 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.7.0 (unreleased) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) + - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) v 8.6.3 (unreleased) - Destroy related todos when an Issue/MR is deleted. !3376 diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 84eefd01cf..c66efe978c 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -43,10 +43,6 @@ .md { color: #7f8fa4; font-size: $gl-font-size; - - iframe.twitter-share-button { - vertical-align: bottom; - } } pre { diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index ed9f603138..f010436bd3 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -52,7 +52,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :require_two_factor_authentication, :two_factor_grace_period, :gravatar_enabled, - :twitter_sharing_enabled, :sign_in_text, :help_page_text, :home_page_url, diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 23693629a4..60a0ff32c9 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -3,10 +3,6 @@ module ApplicationSettingsHelper current_application_settings.gravatar_enabled? end - def twitter_sharing_enabled? - current_application_settings.twitter_sharing_enabled? - end - def signup_enabled? current_application_settings.signup_enabled? end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c4879598c4..052cd87473 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -12,7 +12,6 @@ # updated_at :datetime # home_page_url :string(255) # default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text # version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null @@ -140,7 +139,6 @@ class ApplicationSetting < ActiveRecord::Base default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 0350995d03..de86dacbb1 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -76,13 +76,6 @@ = f.label :gravatar_enabled do = f.check_box :gravatar_enabled Gravatar enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :twitter_sharing_enabled do - = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block' - Twitter enabled - %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter .form-group = f.label :default_projects_limit, class: 'control-label col-sm-2' .col-sm-10 diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 8cf36c711b..5a2a469ba6 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -7,21 +7,3 @@ = link_to_project event.project - else = event.project_name - -- if !event.project.private? && twitter_sharing_enabled? - .event-body{"data-user-is" => event.author_id} - .event-note - .md - %p - Congratulations! Why not share your accomplishment with the world? - - %a.twitter-share-button{ | - href: "https://twitter.com/share", | - "data-url" => event.project.web_url, | - "data-text" => "I just #{event.action_name} a new project on GitLab! GitLab is version control on your server.", | - "data-size" => "medium", | - "data-related" => "gitlab", | - "data-hashtags" => "gitlab", | - "data-count" => "none"} - Tweet - %script{src: "//platform.twitter.com/widgets.js"} diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 626268d764..2b98901527 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -174,7 +174,6 @@ end Settings.gitlab['time_zone'] ||= nil Settings.gitlab['signup_enabled'] ||= true if Settings.gitlab['signup_enabled'].nil? Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].nil? -Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil? Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], []) Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil? Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)' if Settings.gitlab['issue_closing_pattern'].nil? diff --git a/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb new file mode 100644 index 0000000000..0d736e323b --- /dev/null +++ b/db/migrate/20160331223143_remove_twitter_sharing_enabled_from_application_settings.rb @@ -0,0 +1,5 @@ +class RemoveTwitterSharingEnabledFromApplicationSettings < ActiveRecord::Migration + def change + remove_column :application_settings, :twitter_sharing_enabled, :boolean + end +end diff --git a/doc/api/settings.md b/doc/api/settings.md index 001de76c7a..1e745115dc 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -26,7 +26,6 @@ Example response: "default_branch_protection" : 2, "restricted_visibility_levels" : [], "signin_enabled" : true, - "twitter_sharing_enabled" : true, "after_sign_out_path" : null, "max_attachment_size" : 10, "user_oauth_applications" : true, @@ -57,7 +56,6 @@ PUT /application/settings | `sign_in_text` | string | no | Text on login page | | `home_page_url` | string | no | Redirect to this URL when not logged in | | `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push or delete the branch)_, `1` _(partially protected, developers can push new commits, but cannot force push or delete the branch, masters can do anything)_ or `2` _(fully protected, developers cannot push new commits, force push or delete the branch, masters can do anything)_ as a parameter. Default is `1`. | -| `twitter_sharing_enabled` | boolean | no | Allow users to share project creation on Twitter | | `restricted_visibility_levels` | array of integers | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is null which means there is no restriction. | | `max_attachment_size` | integer | no | Limit attachment size in MB | | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | @@ -85,7 +83,6 @@ Example response: "updated_at": "2015-06-30T13:22:42.210Z", "home_page_url": "", "default_branch_protection": 2, - "twitter_sharing_enabled": true, "restricted_visibility_levels": [], "max_attachment_size": 10, "session_expire_delay": 10080, diff --git a/lib/api/entities.rb b/lib/api/entities.rb index f686c568be..b7de575cdc 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -334,7 +334,6 @@ module API expose :updated_at expose :home_page_url expose :default_branch_protection - expose :twitter_sharing_enabled expose :restricted_visibility_levels expose :max_attachment_size expose :session_expire_delay diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 761b63e98f..1acc22fe5b 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -21,7 +21,6 @@ module Gitlab default_branch_protection: Settings.gitlab['default_branch_protection'], signup_enabled: Settings.gitlab['signup_enabled'], signin_enabled: Settings.gitlab['signin_enabled'], - twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'], gravatar_enabled: Settings.gravatar['enabled'], sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index b1764d7ac0..520cf1b75d 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -12,7 +12,6 @@ # updated_at :datetime # home_page_url :string(255) # default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) # restricted_visibility_levels :text # version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null From ff676333d0d257869a917a5e48e9717d219b8311 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 31 Mar 2016 19:28:17 -0500 Subject: [PATCH 333/618] Add current element and event as params to clicked callback --- app/assets/javascripts/gl_dropdown.js.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index e4df7d46d5..d447013f63 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -171,10 +171,11 @@ class GitLabDropdown selector = ".dropdown-page-one .dropdown-content a" @dropdown.on "click", selector, (e) -> - selected = self.rowClicked $(@) + $el = $(@) + selected = self.rowClicked $el if self.options.clicked - self.options.clicked(selected, e) + self.options.clicked(selected, $el, e) # Finds an element inside wrapper element getElement: (selector) -> From ab42560ea6c2fade09b8436a893bf4a269e67c39 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 31 Mar 2016 19:28:41 -0500 Subject: [PATCH 334/618] Return selected object if toggleLabel option is not defined --- app/assets/javascripts/gl_dropdown.js.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index d447013f63..ca8d9a1694 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -352,6 +352,8 @@ class GitLabDropdown # Toggle the dropdown label if @options.toggleLabel $(@el).find(".dropdown-toggle-text").text @options.toggleLabel + else + selectedObject else if !value? field.remove() From 824fecb728ae7534b8ef56aaa6679814f3924cd5 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 31 Mar 2016 19:31:01 -0500 Subject: [PATCH 335/618] Bring back search context when chosing the same project/group --- .../javascripts/search_autocomplete.js.coffee | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 2656c6e30a..564fb265b9 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -104,6 +104,8 @@ class @SearchAutocomplete lastCategory = suggestion.category data.push + id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}" + category: suggestion.category text: suggestion.label url: suggestion.url @@ -271,8 +273,27 @@ class @SearchAutocomplete " @dropdownContent.html(html) - onClick: (item, e) -> + onClick: (item, $el, e) -> if location.pathname.indexOf(item.url) isnt -1 e.preventDefault() + if not @badgePresent + if item.category is 'Projects' + @projectInputEl.val(item.id) + @addLocationBadge( + value: 'This project' + ) + + if item.category is 'Groups' + @groupInputEl.val(item.id) + @addLocationBadge( + value: 'This group' + ) + + $el.removeClass('is-active') @disableAutocomplete() - @searchInput.val('') + @searchInput.val('').focus() + + # We need to wait because of @skipBlurEvent + setTimeout( => + @onSearchInputFocus() + , 200) From 28266d9d610123d20333c6b05fcf83d1fb3e336a Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Fri, 1 Apr 2016 00:17:37 -0300 Subject: [PATCH 336/618] Added WikiLinkFilter --- lib/banzai/filter/wiki_link_filter.rb | 52 +++++++++++++++++++++++++++ lib/banzai/pipeline/wiki_pipeline.rb | 6 ++-- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 lib/banzai/filter/wiki_link_filter.rb diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb new file mode 100644 index 0000000000..7a585eff65 --- /dev/null +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -0,0 +1,52 @@ +require 'uri' + +module Banzai + module Filter + # HTML filter that "fixes" relative links to files in a repository. + # + # Context options: + # :project_wiki + class WikiLinkFilter < HTML::Pipeline::Filter + + def call + return doc unless project_wiki? + + doc.search('a:not(.gfm)').each do |el| + process_link_attr el.attribute('href') + end + + doc + end + + protected + + def project_wiki? + !context[:project_wiki].nil? + end + + def process_link_attr(html_attr) + return if html_attr.blank? + + uri = URI(html_attr.value) + if uri.relative? && uri.path.present? && uri.path + html_attr.value = rebuild_wiki_uri(uri).to_s + end + rescue URI::Error + # noop + end + + def rebuild_wiki_uri(uri) + uri.path = ::File.join(project_wiki_base_path, uri.path) + uri + end + + def project_wiki + context[:project_wiki] + end + + def project_wiki_base_path + project_wiki && project_wiki.wiki_base_path + end + end + end +end diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb index 0b5a9e0b2b..1cdb380896 100644 --- a/lib/banzai/pipeline/wiki_pipeline.rb +++ b/lib/banzai/pipeline/wiki_pipeline.rb @@ -2,8 +2,10 @@ module Banzai module Pipeline class WikiPipeline < FullPipeline def self.filters - @filters ||= super.insert_after(Filter::TableOfContentsFilter, - Filter::GollumTagsFilter) + @filters ||= begin + super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter) + .insert_after(Filter::GollumTagsFilter, Filter::WikiLinkFilter) + end end end end From e7849b0b25390a96881d1f8affd1eadab4e9de62 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 24 Mar 2016 16:41:48 +0100 Subject: [PATCH 337/618] Memoize reference_pattern/link_reference_pattern These methods are called quite often in loops so by memoizing their output we can reduce timings a bit. --- app/models/commit.rb | 4 ++-- app/models/commit_range.rb | 4 ++-- app/models/external_issue.rb | 2 +- app/models/issue.rb | 4 ++-- app/models/label.rb | 2 +- app/models/merge_request.rb | 4 ++-- app/models/milestone.rb | 2 +- app/models/snippet.rb | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/models/commit.rb b/app/models/commit.rb index d0dbe009d0..d09876a07d 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -74,14 +74,14 @@ class Commit # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?\h{7,40}) }x end def self.link_reference_pattern - super("commit", /(?\h{7,40})/) + @link_reference_pattern ||= super("commit", /(?\h{7,40})/) end def to_reference(from_project = nil) diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index 289dbc5728..51673897d9 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -43,14 +43,14 @@ class CommitRange # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?#{STRICT_PATTERN}) }x end def self.link_reference_pattern - super("compare", /(?#{PATTERN})/) + @link_reference_pattern ||= super("compare", /(?#{PATTERN})/) end # Initialize a CommitRange diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 2ca79df0a2..b8585d4e57 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -31,7 +31,7 @@ class ExternalIssue # Pattern used to extract `JIRA-123` issue references from text def self.reference_pattern - %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} + @reference_pattern ||= %r{(?\b([A-Z][A-Z0-9_]+-)\d+)} end def to_reference(_from_project = nil) diff --git a/app/models/issue.rb b/app/models/issue.rb index ed960cb39f..e064b0f8b9 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -73,14 +73,14 @@ class Issue < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\d+) }x end def self.link_reference_pattern - super("issues", /(?\d+)/) + @link_reference_pattern ||= super("issues", /(?\d+)/) end def to_reference(from_project = nil) diff --git a/app/models/label.rb b/app/models/label.rb index 500d5a3552..55c01cae76 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -56,7 +56,7 @@ class Label < ActiveRecord::Base # This pattern supports cross-project references. # def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)} (?: diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 1245cc16d6..45c3b0a3a6 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -149,14 +149,14 @@ class MergeRequest < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\d+) }x end def self.link_reference_pattern - super("merge_requests", /(?\d+)/) + @link_reference_pattern ||= super("merge_requests", /(?\d+)/) end # Returns all the merge requests from an ActiveRecord:Relation. diff --git a/app/models/milestone.rb b/app/models/milestone.rb index bbd59eab9a..07196c0fca 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -79,7 +79,7 @@ class Milestone < ActiveRecord::Base end def self.link_reference_pattern - super("milestones", /(?\d+)/) + @link_reference_pattern ||= super("milestones", /(?\d+)/) end def self.upcoming diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b9e835a448..b96e393728 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -56,14 +56,14 @@ class Snippet < ActiveRecord::Base # # This pattern supports cross-project references. def self.reference_pattern - %r{ + @reference_pattern ||= %r{ (#{Project.reference_pattern})? #{Regexp.escape(reference_prefix)}(?\d+) }x end def self.link_reference_pattern - super("snippets", /(?\d+)/) + @link_reference_pattern ||= super("snippets", /(?\d+)/) end def to_reference(from_project = nil) From 84b0ab77667b85a42db8a5a02d9758657af66f16 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 24 Mar 2016 17:00:26 +0100 Subject: [PATCH 338/618] Added & use Gitlab::Routing for URL helpers Rails' "url_helpers" method creates an anonymous Module (which a bunch of methods) on every call. By caching the output of this method in a dedicated method we can shave off about 10 seconds of loading time for an issue with around 200 comments. --- app/models/milestone.rb | 2 +- app/models/project.rb | 4 ++-- .../gitlab_issue_tracker_service.rb | 2 +- app/models/project_services/jira_service.rb | 2 +- app/services/system_note_service.rb | 2 +- lib/api/entities.rb | 4 ++-- lib/banzai/filter/commit_range_reference_filter.rb | 2 +- lib/banzai/filter/commit_reference_filter.rb | 2 +- lib/banzai/filter/label_reference_filter.rb | 2 +- lib/banzai/filter/merge_request_reference_filter.rb | 2 +- lib/banzai/filter/milestone_reference_filter.rb | 2 +- lib/banzai/filter/snippet_reference_filter.rb | 2 +- lib/banzai/filter/user_reference_filter.rb | 2 +- lib/gitlab/email/message/repository_push.rb | 2 +- lib/gitlab/routing.rb | 13 +++++++++++++ lib/gitlab/url_builder.rb | 2 +- spec/lib/extracts_path_spec.rb | 2 +- spec/lib/gitlab/closing_issue_extractor_spec.rb | 2 +- spec/support/filter_spec_helper.rb | 2 +- spec/support/markdown_feature.rb | 2 +- 20 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 lib/gitlab/routing.rb diff --git a/app/models/milestone.rb b/app/models/milestone.rb index bbd59eab9a..a5d92519b4 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -89,7 +89,7 @@ class Milestone < ActiveRecord::Base def to_reference(from_project = nil) escaped_title = self.title.gsub("]", "\\]") - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers url = h.namespace_project_milestone_url(self.project.namespace, self.project, self) "[#{escaped_title}](#{url})" diff --git a/app/models/project.rb b/app/models/project.rb index f208965086..c5022fd4ff 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -471,7 +471,7 @@ class Project < ActiveRecord::Base end def web_url - Gitlab::Application.routes.url_helpers.namespace_project_url(self.namespace, self) + Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -592,7 +592,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - Gitlab::Application.routes.url_helpers.namespace_project_avatar_url(namespace, self) + Gitlab::Routing.url_helpers.namespace_project_avatar_url(namespace, self) end end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 05436cd0f7..eaa5654b9c 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -20,7 +20,7 @@ # class GitlabIssueTrackerService < IssueTrackerService - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index aba37921c0..1ed42c4f3e 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -21,7 +21,7 @@ class JiraService < IssueTrackerService include HTTParty - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers DEFAULT_API_VERSION = 2 diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e022a046c4..658b086496 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -224,7 +224,7 @@ class SystemNoteService # # "Started branch `issue-branch-button-201`" def self.new_issue_branch(issue, project, author, branch) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) body = "Started branch [`#{branch}`](#{link})" diff --git a/lib/api/entities.rb b/lib/api/entities.rb index f686c568be..c452aed27d 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -8,7 +8,7 @@ module API expose :id, :state, :avatar_url expose :web_url do |user, options| - Gitlab::Application.routes.url_helpers.user_url(user) + Gitlab::Routing.url_helpers.user_url(user) end end @@ -89,7 +89,7 @@ module API expose :avatar_url expose :web_url do |group, options| - Gitlab::Application.routes.url_helpers.group_url(group) + Gitlab::Routing.url_helpers.group_url(group) end end diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb index 470727ee31..b469ea0f62 100644 --- a/lib/banzai/filter/commit_range_reference_filter.rb +++ b/lib/banzai/filter/commit_range_reference_filter.rb @@ -43,7 +43,7 @@ module Banzai end def url_for_object(range, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_compare_url(project.namespace, project, range.to_param.merge(only_path: context[:only_path])) end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 713a56ba94..bd88207326 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -37,7 +37,7 @@ module Banzai end def url_for_object(commit, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_commit_url(project.namespace, project, commit, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 8147e5ed3c..a2987850d0 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -31,7 +31,7 @@ module Banzai end def url_for_object(label, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb index 57c7170899..cad38a5185 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -14,7 +14,7 @@ module Banzai end def url_for_object(mr, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_merge_request_url(project.namespace, project, mr, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 8f710a92bd..4cb8217802 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -11,7 +11,7 @@ module Banzai end def url_for_object(issue, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_milestone_url(project.namespace, project, milestone, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb index c870a42f74..d507eb5ebe 100644 --- a/lib/banzai/filter/snippet_reference_filter.rb +++ b/lib/banzai/filter/snippet_reference_filter.rb @@ -14,7 +14,7 @@ module Banzai end def url_for_object(snippet, project) - h = Gitlab::Application.routes.url_helpers + h = Gitlab::Routing.url_helpers h.namespace_project_snippet_url(project.namespace, project, snippet, only_path: context[:only_path]) end diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 24f16f8b54..989fa64e07 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -90,7 +90,7 @@ module Banzai private def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end def link_class diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb index 41f0edcaf7..8f9be6cd9a 100644 --- a/lib/gitlab/email/message/repository_push.rb +++ b/lib/gitlab/email/message/repository_push.rb @@ -5,7 +5,7 @@ module Gitlab attr_accessor :recipient attr_reader :author_id, :ref, :action - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers delegate :namespace, :name_with_namespace, to: :project, prefix: :project delegate :name, to: :author, prefix: :author diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb new file mode 100644 index 0000000000..5132177de5 --- /dev/null +++ b/lib/gitlab/routing.rb @@ -0,0 +1,13 @@ +module Gitlab + module Routing + # Returns the URL helpers Module. + # + # This method caches the output as Rails' "url_helpers" method creates an + # anonymous module every time it's called. + # + # Returns a Module. + def self.url_helpers + @url_helpers ||= Gitlab::Application.routes.url_helpers + end + end +end diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 6f0d02cafd..22c91be920 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -1,6 +1,6 @@ module Gitlab class UrlBuilder - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers include GitlabRoutingHelper def initialize(type) diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index f38fadda9b..566035c60d 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ExtractsPath, lib: true do include ExtractsPath include RepoHelpers - include Gitlab::Application.routes.url_helpers + include Gitlab::Routing.url_helpers let(:project) { double('project') } diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 844fd79c99..a1f51429a7 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -236,6 +236,6 @@ describe Gitlab::ClosingIssueExtractor, lib: true do end def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index ef5ea7d626..e849a9633b 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -78,6 +78,6 @@ module FilterSpecHelper # Shortcut to Rails' auto-generated routes helpers, to avoid including the # module def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 73c6792b65..b87cd6bbca 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -106,7 +106,7 @@ class MarkdownFeature end def urls - Gitlab::Application.routes.url_helpers + Gitlab::Routing.url_helpers end def raw_markdown From ba0c9b863a6e79d825744bd267a8c984a764235d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 1 Apr 2016 14:48:58 +0200 Subject: [PATCH 339/618] Remove 1px whitespace between nav tabs and underline when form present Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/nav.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 95bdd6d1ea..fc3b0a422a 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -100,6 +100,7 @@ > form { display: inline-block; + margin-top: -1px; } .icon-label { @@ -110,7 +111,7 @@ height: 34px; display: inline-block; position: relative; - top: 1px; + top: 2px; margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ From cd4f3da750a172f14eac914f3f648cf0a803192a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 1 Apr 2016 09:01:44 -0500 Subject: [PATCH 340/618] Add labels to block element --- app/views/projects/ci/builds/_build.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index d22d1da840..2cf9115e4d 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -39,7 +39,7 @@ %td = build.name - .pull-right + .label-container - if build.tags.any? - build.tags.each do |tag| %span.label.label-primary From 9f27b852af789258644ea3a71aa82812af53e91d Mon Sep 17 00:00:00 2001 From: Jacob Schatz Date: Fri, 1 Apr 2016 14:05:16 +0000 Subject: [PATCH 341/618] Revert "Merge branch 'users_should_not_be_able_upvote_downvote' into 'master' " This reverts merge request !3406 --- app/assets/javascripts/awards_handler.coffee | 15 ++------------- app/views/votes/_votes_block.html.haml | 2 +- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index e7ed4605c2..47b080406d 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -22,19 +22,8 @@ class @AwardsHandler emoji = $(this) .find(".icon") .data "emoji" - - if emoji is "thumbsup" and awards_handler.didUserClickEmoji $(this), "thumbsdown" - awards_handler.decrementCounter "thumbsdown" - - else if emoji is "thumbsdown" and awards_handler.didUserClickEmoji $(this), "thumbsup" - awards_handler.decrementCounter "thumbsup" - awards_handler.addAward emoji - didUserClickEmoji: (that, emoji) -> - if $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title") - $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1 - showEmojiMenu: -> if $(".emoji-menu").length if $(".emoji-menu").is ".is-visible" @@ -116,7 +105,7 @@ class @AwardsHandler if origTitle authors = origTitle.split(', ') authors.push("me") - award_block.attr("data-original-title", authors.join(", ")) + award_block.attr("title", authors.join(", ")) @resetTooltip(award_block) resetTooltip: (award) -> @@ -133,7 +122,7 @@ class @AwardsHandler nodes = [] nodes.push( - "" diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 06023f2b2d..0264722977 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,6 +1,6 @@ .awards.votes-block - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| - %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}} + %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}} = emoji_icon(emoji) %span.award-control-text.js-counter = notes.count From bb2214bd64b621e3d0813696535ce4c960612ca1 Mon Sep 17 00:00:00 2001 From: Sandra Freihofer Date: Fri, 1 Apr 2016 16:20:29 +0200 Subject: [PATCH 342/618] Fix award emoji picker for relative_url MR !2888 caused a regression where the emoji picker does not load when using relative_url, because the path for the controller was hardcoded. --- app/assets/javascripts/awards_handler.coffee | 4 ++-- app/views/votes/_votes_block.html.haml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index 47b080406d..6a670d5e88 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,5 +1,5 @@ class @AwardsHandler - constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) -> + constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @aliases) -> $(".js-add-award").on "click", (event) => event.stopPropagation() event.preventDefault() @@ -34,7 +34,7 @@ class @AwardsHandler $("#emoji_search").focus() else $('.js-add-award').addClass "is-loading" - $.get "/emojis", (response) => + $.get @get_emojis_url, (response) => $('.js-add-award').removeClass "is-loading" $(".js-award-holder").append response setTimeout => diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 0264722977..8ffcdc4a32 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -15,12 +15,14 @@ - if current_user :javascript + var get_emojis_url = "#{emojis_path}"; var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; var noteable_type = "#{votable.class.name.underscore}"; var noteable_id = "#{votable.id}"; var aliases = #{AwardEmoji.aliases.to_json}; window.awards_handler = new AwardsHandler( + get_emojis_url, post_emoji_url, noteable_type, noteable_id, From 18dd525d0f163b65893fd3df7d09185e72fc7a1e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 1 Apr 2016 16:58:28 +0200 Subject: [PATCH 343/618] Concept of 2 level navigation sidebar Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/sidebar.scss | 53 +++++ app/views/layouts/nav/_project.html.haml | 215 +++++++++--------- 2 files changed, 155 insertions(+), 113 deletions(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 18189e985c..c042684dd7 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -309,3 +309,56 @@ padding-right: $sidebar_collapsed_width; } } + +.page-sidebar-expanded { + .complex-sidebar { + margin-bottom: 100px; + display: inline-block; + + .nav-sidebar { + margin-bottom: 0; + } + + .nav-primary { + width: 60px; + float: left; + + .nav-sidebar { + width: 60px; + + li a { + width: 60px; + + span { + display: none; + } + } + } + } + + .nav-secondary { + border-left: 1px solid rgba(255, 255, 255, 0.1); + float: left; + width: 168px; + + .nav-sidebar { + width: 168px; + + li a { + width: 168px; + + i { + display: none; + } + } + } + } + } +} + +.page-sidebar-collapsed { + .nav-secondary { + display: none; + transition-duration: .3s; + } +} diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 86b46e8c75..8fdbf5b602 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,126 +1,115 @@ -%ul.nav.nav-sidebar - - if @project.group - = nav_link do - = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to group - - else - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard +.complex-sidebar + .nav-primary + = render 'layouts/nav/dashboard' + .nav-secondary + %ul.nav.nav-sidebar + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do + = icon('bookmark fw') + %span + Project + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + = icon('dashboard fw') + %span + Activity + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do + = icon('files-o fw') + %span + Files - %li.separate-item + - if project_nav_tab? :commits + = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + = icon('history fw') + %span + Commits - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - = icon('bookmark fw') - %span - Project - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - = icon('dashboard fw') - %span - Activity - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do - = icon('files-o fw') - %span - Files + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + = icon('cubes fw') + %span + Builds + %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - = icon('history fw') - %span - Commits + - if project_nav_tab? :graphs + = nav_link(controller: %w(graphs)) do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do + = icon('area-chart fw') + %span + Graphs - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - = icon('cubes fw') - %span - Builds - %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + = icon('clock-o fw') + %span + Milestones - - if project_nav_tab? :graphs - = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do - = icon('area-chart fw') - %span - Graphs + - if project_nav_tab? :issues + = nav_link(controller: :issues) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do + = icon('exclamation-circle fw') + %span + Issues + - if @project.default_issues_tracker? + %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - = icon('clock-o fw') - %span - Milestones + - if project_nav_tab? :merge_requests + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = icon('tasks fw') + %span + Merge Requests + %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - if project_nav_tab? :issues - = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do - = icon('exclamation-circle fw') - %span - Issues - - if @project.default_issues_tracker? - %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + - if project_nav_tab? :settings + = nav_link(controller: [:project_members, :teams]) do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do + = icon('users fw') + %span + Members - - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - = icon('tasks fw') - %span - Merge Requests - %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + = icon('tags fw') + %span + Labels - - if project_nav_tab? :settings - = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do - = icon('users fw') - %span - Members + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + = icon('book fw') + %span + Wiki - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - = icon('tags fw') - %span - Labels + - if project_nav_tab? :forks + = nav_link(controller: :forks, action: :index) do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do + = icon('code-fork fw') + %span + Forks - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - = icon('book fw') - %span - Wiki + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do + = icon('clipboard fw') + %span + Snippets - - if project_nav_tab? :forks - = nav_link(controller: :forks, action: :index) do - = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do - = icon('code-fork fw') - %span - Forks + - if project_nav_tab? :settings + = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do + = link_to edit_project_path(@project), title: 'Settings' do + = icon('cogs fw') + %span + Settings - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do - = icon('clipboard fw') - %span - Snippets - - - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings' do - = icon('cogs fw') - %span - Settings - - -# Global shortcut to network page for compatibility - - if project_nav_tab? :network - %li.hidden - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - Network + -# Global shortcut to network page for compatibility + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Network From dbc96f05d0c7dd6118a0deeea97650417e42917f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 1 Apr 2016 18:24:03 +0200 Subject: [PATCH 344/618] Refactor left navigation sidebar to use 2 levels Signed-off-by: Dmitriy Zaporozhets --- .../stylesheets/framework/gitlab-theme.scss | 1 + app/assets/stylesheets/framework/sidebar.scss | 2 +- app/views/layouts/_page.html.haml | 14 +- app/views/layouts/nav/_dashboard.html.haml | 1 - app/views/layouts/nav/_project.html.haml | 200 +++++++++--------- 5 files changed, 109 insertions(+), 109 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index c83cf88159..ad6ade1304 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -37,6 +37,7 @@ background: $color-darker; .sidebar-user { + border-top: 1px solid $color; background: $color-darker; color: $color-light; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c042684dd7..8d0b0112cd 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -312,7 +312,6 @@ .page-sidebar-expanded { .complex-sidebar { - margin-bottom: 100px; display: inline-block; .nav-sidebar { @@ -337,6 +336,7 @@ } .nav-secondary { + padding-bottom: 100px; border-left: 1px solid rgba(255, 255, 255, 0.1); float: left; width: 168px; diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index c799e9c588..fcf61094a2 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -8,12 +8,16 @@ .gitlab-text-container %h3 GitLab - - if defined?(sidebar) && sidebar - = render "layouts/nav/#{sidebar}" - - elsif current_user - = render 'layouts/nav/dashboard' + - primary_sidebar = current_user ? 'dashboard' : 'explore' + + - if defined?(sidebar) && sidebar && sidebar != primary_sidebar + .complex-sidebar + .nav-primary + = render "layouts/nav/#{primary_sidebar}" + .nav-secondary + = render "layouts/nav/#{sidebar}" - else - = render 'layouts/nav/explore' + = render "layouts/nav/#{primary_sidebar}" .collapse-nav = render partial: 'layouts/collapse_button' diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 4a0069f18f..866ebfc6f0 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -48,7 +48,6 @@ %span Help - %li.separate-item = nav_link(controller: :profile) do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = icon('user fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 8fdbf5b602..d0f82b5f57 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -1,115 +1,111 @@ -.complex-sidebar - .nav-primary - = render 'layouts/nav/dashboard' - .nav-secondary - %ul.nav.nav-sidebar - = nav_link(path: 'projects#show', html_options: {class: 'home'}) do - = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do - = icon('bookmark fw') - %span - Project - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - = icon('dashboard fw') - %span - Activity - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do - = icon('files-o fw') - %span - Files +%ul.nav.nav-sidebar + = nav_link(path: 'projects#show', html_options: {class: 'home'}) do + = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do + = icon('bookmark fw') + %span + Project + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + = icon('dashboard fw') + %span + Activity + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do + = icon('files-o fw') + %span + Files - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - = icon('history fw') - %span - Commits + - if project_nav_tab? :commits + = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + = icon('history fw') + %span + Commits - - if project_nav_tab? :builds - = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - = icon('cubes fw') - %span - Builds - %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) + - if project_nav_tab? :builds + = nav_link(controller: %w(builds)) do + = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + = icon('cubes fw') + %span + Builds + %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) - - if project_nav_tab? :graphs - = nav_link(controller: %w(graphs)) do - = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do - = icon('area-chart fw') - %span - Graphs + - if project_nav_tab? :graphs + = nav_link(controller: %w(graphs)) do + = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do + = icon('area-chart fw') + %span + Graphs - - if project_nav_tab? :milestones - = nav_link(controller: :milestones) do - = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do - = icon('clock-o fw') - %span - Milestones + - if project_nav_tab? :milestones + = nav_link(controller: :milestones) do + = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do + = icon('clock-o fw') + %span + Milestones - - if project_nav_tab? :issues - = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do - = icon('exclamation-circle fw') - %span - Issues - - if @project.default_issues_tracker? - %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) + - if project_nav_tab? :issues + = nav_link(controller: :issues) do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do + = icon('exclamation-circle fw') + %span + Issues + - if @project.default_issues_tracker? + %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count) - - if project_nav_tab? :merge_requests - = nav_link(controller: :merge_requests) do - = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - = icon('tasks fw') - %span - Merge Requests - %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) + - if project_nav_tab? :merge_requests + = nav_link(controller: :merge_requests) do + = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + = icon('tasks fw') + %span + Merge Requests + %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count) - - if project_nav_tab? :settings - = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do - = icon('users fw') - %span - Members + - if project_nav_tab? :settings + = nav_link(controller: [:project_members, :teams]) do + = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do + = icon('users fw') + %span + Members - - if project_nav_tab? :labels - = nav_link(controller: :labels) do - = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do - = icon('tags fw') - %span - Labels + - if project_nav_tab? :labels + = nav_link(controller: :labels) do + = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do + = icon('tags fw') + %span + Labels - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - = icon('book fw') - %span - Wiki + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + = icon('book fw') + %span + Wiki - - if project_nav_tab? :forks - = nav_link(controller: :forks, action: :index) do - = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do - = icon('code-fork fw') - %span - Forks + - if project_nav_tab? :forks + = nav_link(controller: :forks, action: :index) do + = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks' do + = icon('code-fork fw') + %span + Forks - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do - = icon('clipboard fw') - %span - Snippets + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do + = icon('clipboard fw') + %span + Snippets - - if project_nav_tab? :settings - = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), title: 'Settings' do - = icon('cogs fw') - %span - Settings + - if project_nav_tab? :settings + = nav_link(html_options: {class: "#{project_tab_class}"}) do + = link_to edit_project_path(@project), title: 'Settings' do + = icon('cogs fw') + %span + Settings - -# Global shortcut to network page for compatibility - - if project_nav_tab? :network - %li.hidden - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - Network + -# Global shortcut to network page for compatibility + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Network From 8df0653e00d9dd08fee80a6b72d86102f9332f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 1 Apr 2016 18:50:22 +0200 Subject: [PATCH 345/618] Add 8.6.3 items to CHANGELOG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- CHANGELOG | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a4f4959076..75fddce0ed 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,7 +7,6 @@ v 8.7.0 (unreleased) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API - Fix avatar stretching by providing a cropping feature - - Fix raw/rendered diff producing different results on merge requests !3450 - Add links to CI setup documentation from project settings and builds pages - Handle nil descriptions in Slack issue messages (Stan Hu) - Add default scope to projects to exclude projects pending deletion @@ -17,19 +16,18 @@ v 8.7.0 (unreleased) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) -v 8.6.3 (unreleased) +v 8.6.3 + - Mentions on confidential issues doesn't create todos for non-members. !3374 - Destroy related todos when an Issue/MR is deleted. !3376 - Fix error 500 when target is nil on todo list. !3376 - - Allow temporary email as notification email. !TBD - -v 8.6.3 - - Fix copying uploads when moving issue to another project - -v 8.6.3 (unreleased) - - Mentions on confidential issues doesn't create todos for non-members - -v 8.6.3 (unreleased) + - Fix copying uploads when moving issue to another project. !3382 + - Ensuring Merge Request API returns boolean values for work_in_progress (Abhi Rao). !3432 + - Fix raw/rendered diff producing different results on merge requests. !3450 + - Fix commit comment alignment (Stan Hu). !3466 - Fix Error 500 when searching for a comment in a project snippet. !3468 + - Allow temporary email as notification email. !3477 + - Fix issue with dropdowns not selecting values. !3478 + - Update gitlab-shell version and doc to 2.6.12. gitlab-org/gitlab-ee!280 v 8.6.2 - Fix dropdown alignment. !3298 From 03f2dd90ec5c7d9b2f8d9b6254f7853733302896 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 1 Apr 2016 19:32:39 +0200 Subject: [PATCH 346/618] Hide sidebar completely when collapsed Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/sidebar.js.coffee | 1 - app/assets/stylesheets/framework/header.scss | 4 +- app/assets/stylesheets/framework/sidebar.scss | 66 +++++++------------ app/views/layouts/_collapse_button.html.haml | 4 -- app/views/layouts/_page.html.haml | 4 +- 5 files changed, 27 insertions(+), 52 deletions(-) delete mode 100644 app/views/layouts/_collapse_button.html.haml diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 860d4f438d..e177851124 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded' toggleSidebar = -> $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('header').toggleClass("header-collapsed header-expanded") - $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) setTimeout ( -> diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index b3397d1601..724980b220 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -123,11 +123,11 @@ header { } @mixin collapsed-header { - margin-left: $sidebar_collapsed_width; + margin-left: 40px; } .header-collapsed { - margin-left: $sidebar_collapsed_width; + margin-left: 40px; @media (min-width: $screen-md-min) { @include collapsed-header; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 8d0b0112cd..f14433676e 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -191,6 +191,27 @@ } } +.expand-nav a { + color: $gl-icon-color; + width: 60px; + position: fixed; + top: 0; + left: 0; + font-size: 20px; + background: transparent; + height: 59px; + text-align: center; + line-height: 59px; + border-bottom: 1px solid #eee; + transition-duration: .3s; + outline: none; + z-index: 100; + + &:hover { + text-decoration: none; + } +} + .collapse-nav a { width: $sidebar_width; position: fixed; @@ -210,55 +231,12 @@ } .page-sidebar-collapsed { - padding-left: $sidebar_collapsed_width; - .sidebar-wrapper { - width: $sidebar_collapsed_width; - - .header-logo { - width: $sidebar_collapsed_width; - - a { - padding-left: ($sidebar_collapsed_width - 36) / 2; - - .gitlab-text-container { - display: none; - } - } - } - - .nav-sidebar { - width: $sidebar_collapsed_width; - - li { - width: auto; - - a { - span { - display: none; - } - } - } - } - - .collapse-nav a { - width: $sidebar_collapsed_width; - } - - .sidebar-user { - padding-left: ($sidebar_collapsed_width - 36) / 2; - width: $sidebar_collapsed_width; - - .username { - display: none; - } - } + display: none; } } .page-sidebar-expanded { - padding-left: $sidebar_collapsed_width; - @media (min-width: $screen-md-min) { padding-left: $sidebar_width; } diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml deleted file mode 100644 index 2ed51d87ca..0000000000 --- a/app/views/layouts/_collapse_button.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- if nav_menu_collapsed? - = link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close" -- else - = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close" diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index fcf61094a2..8fc1ac8b19 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,7 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } = render "layouts/broadcast" + .expand-nav + = link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open/Close" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo %a#logo @@ -20,7 +22,7 @@ = render "layouts/nav/#{primary_sidebar}" .collapse-nav - = render partial: 'layouts/collapse_button' + = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close" - if current_user = link_to current_user, class: 'sidebar-user', title: "Profile" do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' From de6360f9048511f55a9c93e6fc848418cb6c71a5 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 1 Apr 2016 19:51:18 +0200 Subject: [PATCH 347/618] Improve styling for new complex sidebar Signed-off-by: Dmitriy Zaporozhets --- .../stylesheets/framework/gitlab-theme.scss | 1 - app/assets/stylesheets/framework/sidebar.scss | 65 ++++++++----------- app/views/layouts/_page.html.haml | 4 +- app/views/layouts/nav/_dashboard.html.haml | 2 +- app/views/layouts/nav/_group.html.haml | 10 +-- 5 files changed, 31 insertions(+), 51 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index ad6ade1304..795a26ce34 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -63,7 +63,6 @@ .count { color: $color-light; - background: $color-dark; } } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index f14433676e..e55c9e3e42 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -144,7 +144,7 @@ } a { - padding: 7px 15px; + padding: 7px 12px; font-size: $gl-font-size; line-height: 24px; color: $gray; @@ -169,10 +169,12 @@ } .count { - float: right; - background: #eee; - padding: 0 8px; - @include border-radius(6px); + &:before { + content: '('; + } + &:after { + content: ')'; + } } &.back-link i { @@ -288,55 +290,42 @@ } } -.page-sidebar-expanded { - .complex-sidebar { - display: inline-block; +.complex-sidebar { + display: inline-block; + + .nav-primary { + width: 61px; + float: left; + border-right: 1px solid rgba(255, 255, 255, 0.1); + height: 100vh; .nav-sidebar { - margin-bottom: 0; - } - - .nav-primary { width: 60px; - float: left; - .nav-sidebar { + li a { width: 60px; - li a { - width: 60px; - - span { - display: none; - } + span { + display: none; } } } + } - .nav-secondary { - padding-bottom: 100px; - border-left: 1px solid rgba(255, 255, 255, 0.1); - float: left; + .nav-secondary { + float: left; + width: 168px; + + .nav-sidebar { width: 168px; - .nav-sidebar { + li a { width: 168px; - li a { - width: 168px; - - i { - display: none; - } + i { + display: none; } } } } } - -.page-sidebar-collapsed { - .nav-secondary { - display: none; - transition-duration: .3s; - } -} diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 8fc1ac8b19..9be36273c7 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,7 +1,7 @@ .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } = render "layouts/broadcast" .expand-nav - = link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open/Close" + = link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open sidebar" .sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .header-logo %a#logo @@ -22,7 +22,7 @@ = render "layouts/nav/#{primary_sidebar}" .collapse-nav - = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close" + = link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar" - if current_user = link_to current_user, class: 'sidebar-user', title: "Profile" do = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 866ebfc6f0..dfdabd4c55 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -15,7 +15,7 @@ = icon('dashboard fw') %span Activity - = nav_link(controller: :groups) do + = nav_link(path: ['groups#index']) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 55940741dc..a03d2c703f 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,12 +1,4 @@ %ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do = icon('group fw') @@ -42,7 +34,7 @@ %span Members - if can?(current_user, :admin_group, @group) - = nav_link(html_options: { class: "separate-item" }) do + = nav_link(html_options: { class: "" }) do = link_to edit_group_path(@group), title: 'Settings' do = icon ('cogs fw') %span From 22479fd0ae41d16d7bbd579615ee10c4b22deeed Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 1 Apr 2016 19:53:29 +0200 Subject: [PATCH 348/618] Remove tests with back button Signed-off-by: Dmitriy Zaporozhets --- features/groups.feature | 4 ---- features/project/project.feature | 9 --------- features/steps/groups.rb | 4 ---- features/steps/project/project.rb | 8 -------- 4 files changed, 25 deletions(-) diff --git a/features/groups.feature b/features/groups.feature index 419a5d3963..49e939807b 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -7,10 +7,6 @@ Feature: Groups When I visit group "NonExistentGroup" page Then page status code should be 404 - Scenario: I should have back to group button - When I visit group "Owned" page - Then I should see back to dashboard button - @javascript Scenario: I should see group "Owned" dashboard list When I visit group "Owned" page diff --git a/features/project/project.feature b/features/project/project.feature index f1f3ed2606..aa22401c88 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -18,15 +18,6 @@ Feature: Project Then I should see the default project avatar And I should not see the "Remove avatar" button - Scenario: I should have back to group button - And project "Shop" belongs to group - And I visit project "Shop" page - Then I should see back to group button - - Scenario: I should have back to group button - And I visit project "Shop" page - Then I should see back to dashboard button - Scenario: I should have readme on page And I visit project "Shop" page Then I should see project "Shop" README diff --git a/features/steps/groups.rb b/features/steps/groups.rb index e5b7db4c5e..483370f41c 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -4,10 +4,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedGroup include SharedUser - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - step 'I should see group "Owned"' do expect(page).to have_content '@owned' end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index ef185861e0..d24f3cc306 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -123,14 +123,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps @project.save! end - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - - step 'I should see back to group button' do - expect(page).to have_content 'Go to group' - end - step 'I click notifications drop down button' do click_link 'notifications-button' end From 4a8bd9faaf04a55f84dc350dba6e425301ead716 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 1 Apr 2016 20:34:42 +0200 Subject: [PATCH 349/618] Some tweaks to new sidebar Signed-off-by: Dmitriy Zaporozhets --- .../stylesheets/framework/gitlab-theme.scss | 6 +++++- app/assets/stylesheets/framework/sidebar.scss | 19 ++++++++++++------- app/views/layouts/nav/_admin.html.haml | 2 +- app/views/layouts/nav/_group.html.haml | 2 +- app/views/layouts/nav/_profile.html.haml | 8 -------- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 795a26ce34..fa9038ebac 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -33,11 +33,15 @@ background: $color; } + .complex-sidebar .nav-primary { + border-right: 1px solid lighten($color, 3%); + } + .sidebar-wrapper { background: $color-darker; .sidebar-user { - border-top: 1px solid $color; + border-top: 1px solid lighten($color, 3%); background: $color-darker; color: $color-light; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index e55c9e3e42..c741c826ae 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -296,7 +296,6 @@ .nav-primary { width: 61px; float: left; - border-right: 1px solid rgba(255, 255, 255, 0.1); height: 100vh; .nav-sidebar { @@ -313,17 +312,23 @@ } .nav-secondary { + $nav-secondary-width: 168px; + float: left; - width: 168px; + width: $nav-secondary-width; .nav-sidebar { - width: 168px; + width: $nav-secondary-width; - li a { - width: 168px; + li { + width: $nav-secondary-width; - i { - display: none; + a { + width: $nav-secondary-width; + + i { + display: none; + } } } } diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 280a1b9372..22d1d4d859 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -95,7 +95,7 @@ Spam Logs %span.count= number_with_delimiter(SpamLog.count(:all)) - = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = nav_link(controller: :application_settings) do = link_to admin_application_settings_path, title: 'Settings' do = icon('cogs fw') %span diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index a03d2c703f..0b7de9633e 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -34,7 +34,7 @@ %span Members - if can?(current_user, :admin_group, @group) - = nav_link(html_options: { class: "" }) do + = nav_link do = link_to edit_group_path(@group), title: 'Settings' do = icon ('cogs fw') %span diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index 3b9d31a6fc..cc119fd64e 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -1,12 +1,4 @@ %ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = link_to profile_path, title: 'Profile Settings' do = icon('user fw') From e6b5716785a31396c2aa8b044a087e8b2ca5fac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Fri, 1 Apr 2016 14:21:08 -0500 Subject: [PATCH 350/618] Migrate Repository#local_branches from gitlab-ee. This will help us to avoid posible merge conflicts when merging gitlab-ce to gitlab-ee --- app/models/repository.rb | 16 ++++++++++------ spec/models/repository_spec.rb | 25 +++++++++++++++++++++---- spec/workers/merge_worker_spec.rb | 2 ++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index ff24b75dcd..e7523583d0 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -72,7 +72,7 @@ class Repository return @has_visible_content unless @has_visible_content.nil? @has_visible_content = cache.fetch(:has_visible_content?) do - raw_repository.branch_count > 0 + branch_count > 0 end end @@ -173,7 +173,7 @@ class Repository end def branch_names - cache.fetch(:branch_names) { raw_repository.branch_names } + cache.fetch(:branch_names) { branches.map(&:name) } end def tag_names @@ -191,7 +191,7 @@ class Repository end def branch_count - @branch_count ||= cache.fetch(:branch_count) { raw_repository.branch_count } + @branch_count ||= cache.fetch(:branch_count) { branches.size } end def tag_count @@ -239,7 +239,7 @@ class Repository def expire_branches_cache cache.expire(:branch_names) - @branches = nil + @local_branches = nil end def expire_cache(branch_name = nil, revision = nil) @@ -614,10 +614,14 @@ class Repository refs_contains_sha('tag', sha) end - def branches - @branches ||= raw_repository.branches + def local_branches + @local_branches ||= rugged.branches.each(:local).map do |branch| + Gitlab::Git::Branch.new(branch.name, branch.target) + end end + alias_method :branches, :local_branches + def tags @tags ||= raw_repository.tags end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9242a6f173..c5d5a1c249 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -303,7 +303,7 @@ describe Repository, models: true do describe 'when there are no branches' do before do - allow(repository.raw_repository).to receive(:branch_count).and_return(0) + allow(repository).to receive(:branch_count).and_return(0) end it { is_expected.to eq(false) } @@ -311,13 +311,13 @@ describe Repository, models: true do describe 'when there are branches' do it 'returns true' do - expect(repository.raw_repository).to receive(:branch_count).and_return(3) + expect(repository).to receive(:branch_count).and_return(3) expect(subject).to eq(true) end it 'caches the output' do - expect(repository.raw_repository).to receive(:branch_count). + expect(repository).to receive(:branch_count). once. and_return(3) @@ -436,7 +436,7 @@ describe Repository, models: true do it 'expires the visible content cache' do repository.has_visible_content? - expect(repository.raw_repository).to receive(:branch_count). + expect(repository).to receive(:branch_count). once. and_return(0) @@ -865,4 +865,21 @@ describe Repository, models: true do repository.build_cache end end + + describe '#local_branches' do + it 'returns the local branches' do + masterrev = repository.find_branch('master').target + create_remote_branch('joe', 'remote_branch', masterrev) + repository.add_branch(user, 'local_branch', masterrev) + + expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) + expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) + end + end + + def create_remote_branch(remote_name, branch_name, target) + rugged = repository.rugged + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target) + end + end diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb index b11c5de94e..1abd87d7d3 100644 --- a/spec/workers/merge_worker_spec.rb +++ b/spec/workers/merge_worker_spec.rb @@ -22,6 +22,8 @@ describe MergeWorker do merge_request.reload expect(merge_request).to be_merged + + source_project.repository.expire_branches_cache expect(source_project.repository.branch_names).not_to include('markdown') end end From 847940516b73977c2471d830e64a439f2d226685 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 1 Apr 2016 22:35:49 +0200 Subject: [PATCH 351/618] Fix some active tab tests that are broken because of 2 level sidebar Signed-off-by: Dmitriy Zaporozhets --- app/views/layouts/nav/_dashboard.html.haml | 2 +- features/steps/group/milestones.rb | 4 +++- features/steps/project/active_tab.rb | 4 +++- features/steps/shared/project_tab.rb | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index dfdabd4c55..dc2917df0b 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -15,7 +15,7 @@ = icon('dashboard fw') %span Activity - = nav_link(path: ['groups#index']) do + = nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index a167d25983..f047669ba3 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -5,7 +5,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps include SharedUser step 'I click on group milestones' do - click_link 'Milestones' + within '.nav-secondary' do + click_link 'Milestones' + end end step 'I should see group milestones index page has no milestones' do diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 19d81453d8..b08eb45a45 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -82,7 +82,9 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Issues step 'I click the "Milestones" tab' do - click_link('Milestones') + within '.nav-secondary' do + click_link('Milestones') + end end step 'I click the "Labels" tab' do diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index 4fc2ece79f..fa7d24ce61 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -41,7 +41,7 @@ module SharedProjectTab end step 'the active main tab should be Settings' do - page.within '.nav-sidebar' do + page.within '.nav-secondary' do expect(page).to have_content('Go to project') end end From fb8b0041d86080f0f8d53f13be1016b0b199a47f Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Mar 2016 18:53:30 -0400 Subject: [PATCH 352/618] First pass at a Testing styleguide [ci skip] --- CONTRIBUTING.md | 2 +- doc/development/README.md | 1 + doc/development/testing.md | 105 +++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 doc/development/testing.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 511336f384..1f26a5d7ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -448,7 +448,7 @@ merge request: - multi-line method chaining style **Option B**: dot `.` on previous line - string literal quoting style **Option A**: single quoted by default 1. [Rails](https://github.com/bbatsov/rails-style-guide) -1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing) +1. [Testing](doc/development/testing.md) 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript) 1. [SCSS styleguide][scss-styleguide] 1. [Shell commands](doc/development/shell_commands.md) created by GitLab diff --git a/doc/development/README.md b/doc/development/README.md index 1b281809af..a8bc4fe5ab 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -9,4 +9,5 @@ - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) - [SQL guidelines](sql.md) for SQL guidelines +- [Testing standards and style guidelines](testing.md) - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements diff --git a/doc/development/testing.md b/doc/development/testing.md new file mode 100644 index 0000000000..d37ef5d878 --- /dev/null +++ b/doc/development/testing.md @@ -0,0 +1,105 @@ +# Testing Standards and Style Guidelines + +This guide outlines standards and best practices for automated testing of GitLab +CE and EE. + +It is meant to be an _extension_ of the [thoughtbot testing +styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If +this guide defines a rule that contradicts the thoughtbot guide, this guide +takes precedence. Some guidelines may be repeated verbatim to stress their +importance. + +## Factories + +GitLab uses [factory_girl] as a test +fixture replacement. + +- Factory definitions live in `spec/factories/`, named using the pluralization + of their corresponding model (`User` factories are defined in `users.rb`). +- There should be only one top-level factory definition per file. +- Make use of [Traits] to clean up definitions and usages. +- When defining a factory, don't define attributes that are not required for the + resulting record to pass validation. +- When instantiating from a factory, don't supply extraneous attributes that + aren't required by the test. + +[factory_girl]: https://github.com/thoughtbot/factory_girl +[Traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits + +## JavaScript + +GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on +the command line via `bundle exec teaspoon`, or via a web browser at +`http://localhost:3000/teaspoon` when the Rails server is running. + +- JavaScript tests live in `spec/javascripts/`, matching the folder structure of + `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding + `spec/javascripts/behaviors/autosize_spec.js.coffee` file. +- Haml fixtures required for JavaScript tests live in + `spec/javascripts/fixtures`. They should contain the bare minimum amount of + markup necessary for the test. + + > **Warning:** Keep in mind that a Rails view may change and + invalidate your test, but everything will still pass because your fixture + doesn't reflect the latest view. + +- Keep in mind that in a CI environment, these tests are run in a headless + browser and you will not have access to certain APIs, such as + [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), + which will have to be stubbed. + +[Teaspoon]: https://github.com/modeset/teaspoon +[Jasmine]: https://github.com/jasmine/jasmine + +## RSpec + +### General Guidelines + +- Use a single, top-level `describe ClassName` block. +- Use `described_class` instead of repeating the class name being described. +- Use `.method` to describe class methods and `#method` to describe instance + methods. +- Use `context` to test branching logic. +- Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). +- Prefer `not_to` to `to_not`. +- Try to match the ordering of tests to the ordering within the class. + +### Test speed + +GitLab has a massive test suite that, without parallelization, can take more +than an hour to run. It's important that we make an effort to write tests that +are accurate and effective _as well as_ fast. + +Here are some things to keep in mind regarding test performance: + +- `double` and `spy` are faster than `FactoryGirl.build(...)` +- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`. +- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`, + `spy`, or `double` will do. Database persistence is slow! +- Use `create(:empty_project)` instead of `create(:project)` when you don't need + the underlying repository. Filesystem operations are slow! +- Don't mark a feature as requiring JavaScript (through `@javascript` in + Spinach or `js: true` in RSpec) unless it's _actually_ required for the test + to be valid. Headless browser testing is slow! + +### Features / Integration + +- Feature specs live in `spec/features/` and should be named + `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`. +- Use only one `feature` block per feature spec file. +- Use scenario titles that describe the success and failure paths. +- Avoid scenario titles that add no information, such as "successfully." +- Avoid scenario titles that repeat the feature title. + +## Spinach (feature) tests + +GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426) +for its feature/integration tests in September 2012. + +As of March 2016, we are [trying to avoid adding new Spinach +tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward, +opting for [RSpec feature](#features-integration) specs. + +Adding new Spinach scenarios is acceptable _only if_ the new scenario requires +no more than one new `step` definition. If more than that is required, the +test should be re-implemented using RSpec instead. From 0b9d9816f8ef1d871a050cea5d8bc3d9203c3d18 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Wed, 30 Mar 2016 19:11:02 -0400 Subject: [PATCH 353/618] Add a note about Four-Phase Test to Testing guide [ci skip] --- doc/development/testing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/development/testing.md b/doc/development/testing.md index d37ef5d878..a7e85ecebe 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -63,6 +63,8 @@ the command line via `bundle exec teaspoon`, or via a web browser at - Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). - Prefer `not_to` to `to_not`. - Try to match the ordering of tests to the ordering within the class. +- Try to follow the [Four-Phase Test](https://robots.thoughtbot.com/four-phase-test) + pattern, using newlines to separate phases. ### Test speed From 60f4081e135bdcd893d60192e652c4b829c656dd Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Apr 2016 20:29:48 -0400 Subject: [PATCH 354/618] Factories don't have to be limited to `ActiveRecord` objects [ci skip] --- doc/development/testing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/development/testing.md b/doc/development/testing.md index a7e85ecebe..3d1c4ccab4 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -22,6 +22,8 @@ fixture replacement. resulting record to pass validation. - When instantiating from a factory, don't supply extraneous attributes that aren't required by the test. +- Factories don't have to be limited to `ActiveRecord` objects. + [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d). [factory_girl]: https://github.com/thoughtbot/factory_girl [Traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits From 2e83b562030e078ce6d37f81915590426a57c820 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Apr 2016 20:30:24 -0400 Subject: [PATCH 355/618] Add a section about `let` to the Testing guide [ci skip] --- doc/development/testing.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/doc/development/testing.md b/doc/development/testing.md index 3d1c4ccab4..187d852e53 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -11,8 +11,7 @@ importance. ## Factories -GitLab uses [factory_girl] as a test -fixture replacement. +GitLab uses [factory_girl] as a test fixture replacement. - Factory definitions live in `spec/factories/`, named using the pluralization of their corresponding model (`User` factories are defined in `users.rb`). @@ -65,8 +64,30 @@ the command line via `bundle exec teaspoon`, or via a web browser at - Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). - Prefer `not_to` to `to_not`. - Try to match the ordering of tests to the ordering within the class. -- Try to follow the [Four-Phase Test](https://robots.thoughtbot.com/four-phase-test) - pattern, using newlines to separate phases. +- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines + to separate phases. + +[four-phase-test]: https://robots.thoughtbot.com/four-phase-test + +### `let` variables + +GitLab's RSpec suite has made extensive use of `let` variables to reduce +duplication. However, this sometimes [comes at the cost of clarity][lets-not], +so we need to set some guidelines for their use going forward: + +- `let` variables are preferable to instance variables. Local variables are + preferable to `let` variables. +- Use `let` to reduce duplication throughout an entire spec file. +- Don't use `let` to define variables used by a single test; define them as + local variables inside the test's `it` block. +- Don't define a `let` variable inside the top-level `describe` block that's + only used in a more deeply-nested `context` or `describe` block. Keep the + definition as close as possible to where it's used. +- Try to avoid overriding the definition of one `let` variable with another. +- Don't define a `let` variable that's only used by the definition of another. + Use a helper method instead. + +[lets-not]: https://robots.thoughtbot.com/lets-not ### Test speed From 6ffda88c973ce9be263d0e9142a3105f2cccf000 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 1 Apr 2016 20:36:44 -0400 Subject: [PATCH 356/618] Add a link back to Development documentation to Testing guide [ci skip] --- doc/development/testing.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/development/testing.md b/doc/development/testing.md index 187d852e53..23417845f1 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -128,3 +128,7 @@ opting for [RSpec feature](#features-integration) specs. Adding new Spinach scenarios is acceptable _only if_ the new scenario requires no more than one new `step` definition. If more than that is required, the test should be re-implemented using RSpec instead. + +--- + +[Return to Development documentation](README.md) From b43e0597488d1beff0ed8510f56dbebb38382152 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 2 Apr 2016 00:45:07 -0700 Subject: [PATCH 357/618] Don't fetch any tags from a forked repo Closes #13957 --- CHANGELOG | 1 + app/models/repository.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5f73570650..32ca386630 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - Don't attempt to fetch any tags from a forked repo (Stan Hu) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) diff --git a/app/models/repository.rb b/app/models/repository.rb index ff24b75dcd..bf76de6114 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -820,7 +820,7 @@ class Repository end def fetch_ref(source_path, source_ref, target_ref) - args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref}) + args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) end From 31b27adeb864ae5f057061b1c208005be181dac4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 2 Apr 2016 11:26:46 +0200 Subject: [PATCH 358/618] Fix milestones tab active state and tests Signed-off-by: Dmitriy Zaporozhets --- app/views/layouts/nav/_dashboard.html.haml | 2 +- features/steps/group/milestones.rb | 4 ++-- features/steps/project/active_tab.rb | 2 +- features/steps/project/fork.rb | 2 +- features/steps/project/project.rb | 4 +++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index dc2917df0b..d1a180e429 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -20,7 +20,7 @@ = icon('group fw') %span Groups - = nav_link(controller: :milestones) do + = nav_link(path: 'dashboard#milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') %span diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index f047669ba3..b6ce5bc9ce 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -5,8 +5,8 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps include SharedUser step 'I click on group milestones' do - within '.nav-secondary' do - click_link 'Milestones' + page.within '.nav-secondary' do + click_link("Milestones") end end diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index b08eb45a45..4584fc4d75 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -82,7 +82,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps # Sub Tabs: Issues step 'I click the "Milestones" tab' do - within '.nav-secondary' do + page.within '.nav-secondary' do click_link('Milestones') end end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 527f7853da..d9b16afa9b 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I goto the Merge Requests page' do - page.within '.page-sidebar-expanded' do + page.within '.nav-secondary' do click_link "Merge Requests" end end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index d24f3cc306..8f1d4a223a 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -114,7 +114,9 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should not see "Snippets" button' do - expect(page).not_to have_link 'Snippets' + page.within '.nav-secondary' do + expect(page).not_to have_link 'Snippets' + end end step 'project "Shop" belongs to group' do From fd090a2fab64332907953a1525cf5c822d983993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Sat, 2 Apr 2016 07:36:41 -0500 Subject: [PATCH 359/618] Fix bug related to filtering Issues by Label/Milestone. This problem only was affecting the dev env. --- app/models/concerns/issuable.rb | 2 +- features/dashboard/dashboard.feature | 8 ++++++++ features/steps/dashboard/dashboard.rb | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 47ac22995a..afa2ca039a 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -42,7 +42,7 @@ module Issuable scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } - scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) } + scope :non_archived, -> { join_project.where(projects: { archived: false }) } delegate :name, :email, diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index c3b3577c44..db73309804 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -6,6 +6,7 @@ Feature: Dashboard And project "Shop" has push event And project "Shop" has CI enabled And project "Shop" has CI build + And project "Shop" has labels: "bug", "feature", "enhancement" And I visit dashboard page Scenario: I should see projects list @@ -50,6 +51,13 @@ Feature: Dashboard And I visit dashboard issues page Then The list should be sorted by "Oldest updated" + @javascript + Scenario: Filtering Issues by label + Given project "Shop" has issue "Bugfix1" with label "feature" + When I visit dashboard issues page + And I filter the list by label "feature" + Then I should see "Bugfix1" in issues list + @javascript Scenario: Visiting Project's issues after sorting Given I visit dashboard issues page diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index 5062e34884..b5980b3510 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -87,4 +87,23 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I should see 1 project at group list' do expect(find('span.last_activity/span')).to have_content('1') end + + step 'I filter the list by label "feature"' do + page.within ".labels-filter" do + find('.dropdown').click + click_link "feature" + end + end + + step 'I should see "Bugfix1" in issues list' do + page.within "ul.content-list" do + expect(page).to have_content "Bugfix1" + end + end + + step 'project "Shop" has issue "Bugfix1" with label "feature"' do + project = Project.find_by(name: "Shop") + issue = create(:issue, title: "Bugfix1", project: project, assignee: current_user) + issue.labels << project.labels.find_by(title: 'feature') + end end From deca9fc6266dfd5588b23f64157ddb05e5713412 Mon Sep 17 00:00:00 2001 From: Alessio Biancalana Date: Sat, 2 Apr 2016 14:44:50 +0200 Subject: [PATCH 360/618] Added bottom margin to CLI instructions --- app/assets/stylesheets/pages/projects.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 4e6aa8cd1a..61150746c7 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -315,7 +315,7 @@ pre.light-well { } .git-empty { - margin: 0 7px; + margin: 0 7px 7px; h5 { color: #5c5d5e; From 261c8e765f5b13dd627fcda5ca1ae263ecfad0c8 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Thu, 31 Mar 2016 23:33:32 +0100 Subject: [PATCH 361/618] fix missing filters on status tab when user swithches to another state --- app/assets/javascripts/issues.js.coffee | 15 +++++++++++ app/assets/javascripts/lib/url_utility.js | 33 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 app/assets/javascripts/lib/url_utility.js diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index b1479bfb44..8d069a209d 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -26,6 +26,20 @@ $(".selected_issue").bind "change", Issues.checkChanged + # Update state filters if present in page + updateStateFilters: -> + stateFilters = $('.issues-state-filters') + newParams = {} + paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search'] + + for paramKey in paramKeys + newParams[paramKey] = getUrlParameter(paramKey) or '' + + if stateFilters.length + stateFilters.find('a').each -> + initialUrl = $(this).attr 'href' + $(this).attr 'href', mergeUrlParams(newParams, initialUrl) + # Make sure we trigger ajax request only after user stop typing initSearch: -> @timer = null @@ -54,6 +68,7 @@ # Change url so if user reload a page - search results are saved history.replaceState {page: issuesUrl}, document.title, issuesUrl Issues.reload() + Issues.updateStateFilters() dataType: "json" checkChanged: -> diff --git a/app/assets/javascripts/lib/url_utility.js b/app/assets/javascripts/lib/url_utility.js new file mode 100644 index 0000000000..5fa3a4c69d --- /dev/null +++ b/app/assets/javascripts/lib/url_utility.js @@ -0,0 +1,33 @@ +function getUrlParameter(sParam) { + var sPageURL = decodeURIComponent(window.location.search.substring(1)), + sURLVariables = sPageURL.split('&'), + sParameterName, + i; + + for (i = 0; i < sURLVariables.length; i++) { + sParameterName = sURLVariables[i].split('='); + + if (sParameterName[0] === sParam) { + return sParameterName[1] === undefined ? true : sParameterName[1]; + } + } +} + +/** + * @param {Object} params - url keys and value to merge + * @param {String} url + */ +function mergeUrlParams(params, url){ + var newUrl = decodeURIComponent(url); + + Object.keys(params).forEach(function(paramName) { + var pattern = new RegExp('\\b('+paramName+'=).*?(&|$)') + if (url.search(pattern) >= 0){ + newUrl = newUrl.replace(pattern,'$1' + params[paramName] + '$2'); + } else { + newUrl = newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + '=' + params[paramName] + } + }); + + return newUrl; +} \ No newline at end of file From db8836ca84ab86dc39b4d8b3282603c724a78e67 Mon Sep 17 00:00:00 2001 From: Arinde Eniola Date: Sat, 2 Apr 2016 16:56:31 +0100 Subject: [PATCH 362/618] attach the utitlity function to the global scope with some changes --- app/assets/javascripts/issues.js.coffee | 4 +-- app/assets/javascripts/lib/url_utility.js | 33 ------------------- .../javascripts/lib/url_utility.js.coffee | 31 +++++++++++++++++ 3 files changed, 33 insertions(+), 35 deletions(-) delete mode 100644 app/assets/javascripts/lib/url_utility.js create mode 100644 app/assets/javascripts/lib/url_utility.js.coffee diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index 8d069a209d..0d9f2094c2 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -33,12 +33,12 @@ paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search'] for paramKey in paramKeys - newParams[paramKey] = getUrlParameter(paramKey) or '' + newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or '' if stateFilters.length stateFilters.find('a').each -> initialUrl = $(this).attr 'href' - $(this).attr 'href', mergeUrlParams(newParams, initialUrl) + $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl) # Make sure we trigger ajax request only after user stop typing initSearch: -> diff --git a/app/assets/javascripts/lib/url_utility.js b/app/assets/javascripts/lib/url_utility.js deleted file mode 100644 index 5fa3a4c69d..0000000000 --- a/app/assets/javascripts/lib/url_utility.js +++ /dev/null @@ -1,33 +0,0 @@ -function getUrlParameter(sParam) { - var sPageURL = decodeURIComponent(window.location.search.substring(1)), - sURLVariables = sPageURL.split('&'), - sParameterName, - i; - - for (i = 0; i < sURLVariables.length; i++) { - sParameterName = sURLVariables[i].split('='); - - if (sParameterName[0] === sParam) { - return sParameterName[1] === undefined ? true : sParameterName[1]; - } - } -} - -/** - * @param {Object} params - url keys and value to merge - * @param {String} url - */ -function mergeUrlParams(params, url){ - var newUrl = decodeURIComponent(url); - - Object.keys(params).forEach(function(paramName) { - var pattern = new RegExp('\\b('+paramName+'=).*?(&|$)') - if (url.search(pattern) >= 0){ - newUrl = newUrl.replace(pattern,'$1' + params[paramName] + '$2'); - } else { - newUrl = newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + '=' + params[paramName] - } - }); - - return newUrl; -} \ No newline at end of file diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee new file mode 100644 index 0000000000..abd556e0b4 --- /dev/null +++ b/app/assets/javascripts/lib/url_utility.js.coffee @@ -0,0 +1,31 @@ +((w) -> + + w.gl ?= {} + w.gl.utils ?= {} + + w.gl.utils.getUrlParameter = (sParam) -> + sPageURL = decodeURIComponent(window.location.search.substring(1)) + sURLVariables = sPageURL.split('&') + sParameterName = undefined + i = 0 + while i < sURLVariables.length + sParameterName = sURLVariables[i].split('=') + if sParameterName[0] is sParam + return if sParameterName[1] is undefined then true else sParameterName[1] + i++ + + # # + # @param {Object} params - url keys and value to merge + # @param {String} url + # # + w.gl.utils.mergeUrlParams = (params, url) -> + newUrl = decodeURIComponent(url) + for paramName, paramValue of params + pattern = new RegExp "\\b(#{paramName}=).*?(&|$)" + if url.search(pattern) >= 0 + newUrl = newUrl.replace pattern, "$1#{paramValue}$2" + else + newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}" + newUrl + +) window From 65fa7161736f6688719b6e66952c611770a64473 Mon Sep 17 00:00:00 2001 From: Baldinof Date: Sun, 3 Apr 2016 15:17:14 +0000 Subject: [PATCH 363/618] Fix rubocop in unlink fork service specs --- spec/services/projects/unlink_fork_service_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index f287b0a59b..23f5555d3e 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -25,8 +25,8 @@ describe Projects::UnlinkForkService, services: true do end it 'remove fork relation' do - expect(fork_project.forked_project_link).to receive(:destroy) + expect(fork_project.forked_project_link).to receive(:destroy) - subject.execute + subject.execute end end From 57bde0ce65caf2cbbb6a57f21435639cdaa06225 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 24 Mar 2016 15:53:38 +0100 Subject: [PATCH 364/618] Cache Banzai projects/objects using RequestStore This was originally suggested by @ayufan and modified to be a bit cleaner and use RequestStore instead of a regular Hash. By caching the output of the two methods involved the number of queries is reduced significantly. For example, for an issue with 200 notes (of which 100 reference a number of merge requests) this cuts down the amount of queries from around 6300 to around 3300. --- Gemfile | 2 +- Gemfile.lock | 4 +- .../filter/abstract_reference_filter.rb | 71 +++++++++++++++++-- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/Gemfile b/Gemfile index 006e53e0c1..6327227282 100644 --- a/Gemfile +++ b/Gemfile @@ -214,7 +214,7 @@ gem 'jquery-rails', '~> 4.0.0' gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 5.0.0' gem 'raphael-rails', '~> 2.1.2' -gem 'request_store', '~> 1.2.0' +gem 'request_store', '~> 1.3.0' gem 'select2-rails', '~> 3.5.9' gem 'virtus', '~> 1.0.1' gem 'net-ssh', '~> 3.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index bd41cc8419..229089f431 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -652,7 +652,7 @@ GEM redis-store (~> 1.1.0) redis-store (1.1.7) redis (>= 2.2) - request_store (1.2.1) + request_store (1.3.0) rerun (0.11.0) listen (~> 3.0) responders (2.1.1) @@ -1011,7 +1011,7 @@ DEPENDENCIES redcarpet (~> 3.3.3) redis-namespace redis-rails (~> 4.0.0) - request_store (~> 1.2.0) + request_store (~> 1.3.0) rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 1.10.1) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 34c3891347..41fd4be76a 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -62,11 +62,53 @@ module Banzai # Example: project.merge_requests.find end + def find_object_cached(project, id) + if RequestStore.active? + cache = find_objects_cache[object_class][project.id] + + if cache.key?(id) + cache[id] + else + cache[id] = find_object(project, id) + end + else + find_object(project, id) + end + end + + def project_from_ref_cache(ref) + if RequestStore.active? + cache = project_refs_cache + + if cache.key?(ref) + cache[ref] + else + cache[ref] = project_from_ref(ref) + end + else + project_from_ref(ref) + end + end + def url_for_object(object, project) # Implement in child class # Example: project_merge_request_url end + def url_for_object_cached(object, project) + if RequestStore.active? + cache = url_for_object_cache[object_class][project.id] + + if cache.key?(object) + cache[object] + else + cache[object] = url_for_object(object, project) + end + else + url_for_object(object, project) + end + end + def call if object_class.reference_pattern # `#123` @@ -109,9 +151,9 @@ module Banzai # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. def object_link_filter(text, pattern, link_text: nil) references_in(text, pattern) do |match, id, project_ref, matches| - project = project_from_ref(project_ref) + project = project_from_ref_cache(project_ref) - if project && object = find_object(project, id) + if project && object = find_object_cached(project, id) title = object_link_title(object) klass = reference_class(object_sym) @@ -121,8 +163,11 @@ module Banzai object_sym => object.id ) - url = matches[:url] if matches.names.include?("url") - url ||= url_for_object(object, project) + if matches.names.include?("url") && matches[:url] + url = matches[:url] + else + url = url_for_object_cached(object, project) + end text = link_text || object_link_text(object, matches) @@ -157,6 +202,24 @@ module Banzai text end + + private + + def project_refs_cache + RequestStore[:banzai_project_refs] ||= {} + end + + def find_objects_cache + RequestStore[:banzai_find_objects_cache] ||= Hash.new do |hash, key| + hash[key] = Hash.new { |h, k| h[k] = {} } + end + end + + def url_for_object_cache + RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key| + hash[key] = Hash.new { |h, k| h[k] = {} } + end + end end end end From 141148057703048f5c409c040c80c277f7747273 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 24 Mar 2016 15:57:22 +0100 Subject: [PATCH 365/618] Refactor processing of various Banzai filters These filters now use a single iteration over all the document nodes instead of multiple ones. This in turn allows variables to be re-used (e.g. links only have to be unescaped once). Combined with some other refactoring this can drastically reduce render timings. --- .../filter/abstract_reference_filter.rb | 57 +++--- .../filter/external_issue_reference_filter.rb | 24 ++- lib/banzai/filter/reference_filter.rb | 176 ++++++------------ lib/banzai/filter/user_reference_filter.rb | 25 ++- 4 files changed, 134 insertions(+), 148 deletions(-) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 41fd4be76a..16b703a332 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -110,30 +110,45 @@ module Banzai end def call - if object_class.reference_pattern - # `#123` - replace_text_nodes_matching(object_class.reference_pattern) do |content| - object_link_filter(content, object_class.reference_pattern) - end + return doc if project.nil? - # `[Issue](#123)`, which is turned into - # `Issue` - replace_link_nodes_with_href(object_class.reference_pattern) do |link, text| - object_link_filter(link, object_class.reference_pattern, link_text: text) - end - end + ref_pattern = object_class.reference_pattern + link_pattern = object_class.link_reference_pattern - if object_class.link_reference_pattern - # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into - # `http://gitlab.example.com/namespace/project/issues/123` - replace_link_nodes_with_text(object_class.link_reference_pattern) do |text| - object_link_filter(text, object_class.link_reference_pattern) - end + each_node do |node| + if text_node?(node) && ref_pattern + replace_text_when_pattern_matches(node, ref_pattern) do |content| + object_link_filter(content, ref_pattern) + end - # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into - # `Issue` - replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text| - object_link_filter(link, object_class.link_reference_pattern, link_text: text) + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if ref_pattern && link =~ /\A#{ref_pattern}/ + replace_link_node_with_href(node, link) do + object_link_filter(link, ref_pattern, link_text: text) + end + + next + end + + next unless link_pattern + + if link == text && text =~ /\A#{link_pattern}/ + replace_link_node_with_text(node, link) do + object_link_filter(text, link_pattern) + end + + next + end + + if link =~ /\A#{link_pattern}\z/ + replace_link_node_with_href(node, link) do + object_link_filter(link, link_pattern, link_text: text) + end + + next + end + end end end diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index edc2638690..a3b66c4645 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -37,13 +37,27 @@ module Banzai # Early return if the project isn't using an external tracker return doc if project.nil? || project.default_issues_tracker? - replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content| - issue_link_filter(content) + ref_pattern = ExternalIssue.reference_pattern + ref_start_pattern = /\A#{ref_pattern}\z/ + + each_node do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + issue_link_filter(content) + end + + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if link =~ ref_start_pattern + replace_link_node_with_href(node, link) do + issue_link_filter(link, link_text: text) + end + end + end + end end - replace_link_nodes_with_href(ExternalIssue.reference_pattern) do |link, text| - issue_link_filter(link, link_text: text) - end + doc end # Replace `JIRA-123` issue references in text with links to the referenced diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index a3326ae042..31386cf851 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -52,18 +52,13 @@ module Banzai html.html_safe? ? html : ERB::Util.html_escape_once(html) end - def ignore_parents - @ignore_parents ||= begin - # Don't look for references in text nodes that are children of these - # elements. + def ignore_ancestor_query + @ignore_ancestor_query ||= begin parents = %w(pre code a style) parents << 'blockquote' if context[:ignore_blockquotes] - parents.to_set - end - end - def ignored_ancestry?(node) - has_ancestor?(node, ignore_parents) + parents.map { |n| "ancestor::#{n}" }.join(' or ') + end end def project @@ -74,120 +69,67 @@ module Banzai "gfm gfm-#{type}" end - # Iterate through the document's text nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's content matches `pattern` AND - # * The node is not an ancestor of an ignored node type - # - # pattern - Regex pattern against which to match the node's content - # - # Yields the current node's String contents. The result of the block will - # replace the node's existing content and update the current document. - # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_text_nodes_matching(pattern) - return doc if project.nil? - - search_text_nodes(doc).each do |node| - next if ignored_ancestry?(node) - next unless node.text =~ pattern - - content = node.to_html - - html = yield content - - next if html == content - - node.replace(html) - end - - doc - end - - # Iterate through the document's link nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's content matches `pattern` - # - # pattern - Regex pattern against which to match the node's content - # - # Yields the current node's String contents. The result of the block will - # replace the node and update the current document. - # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_link_nodes_with_text(pattern) - return doc if project.nil? - - doc.xpath('descendant-or-self::a').each do |node| - klass = node.attr('class') - next if klass && klass.include?('gfm') - - link = node.attr('href') - text = node.text - - next unless link && text - - link = CGI.unescape(link) - next unless link.force_encoding('UTF-8').valid_encoding? - # Ignore ending punctionation like periods or commas - next unless link == text && text =~ /\A#{pattern}/ - - html = yield text - - next if html == text - - node.replace(html) - end - - doc - end - - # Iterate through the document's link nodes, yielding the current node's - # content if: - # - # * The `project` context value is present AND - # * The node's HREF matches `pattern` - # - # pattern - Regex pattern against which to match the node's HREF - # - # Yields the current node's String HREF and String content. - # The result of the block will replace the node and update the current document. - # - # Returns the updated Nokogiri::HTML::DocumentFragment object. - def replace_link_nodes_with_href(pattern) - return doc if project.nil? - - doc.xpath('descendant-or-self::a').each do |node| - klass = node.attr('class') - next if klass && klass.include?('gfm') - - link = node.attr('href') - text = node.text - - next unless link && text - link = CGI.unescape(link) - next unless link.force_encoding('UTF-8').valid_encoding? - next unless link && link =~ /\A#{pattern}\z/ - - html = yield link, text - - next if html == link - - node.replace(html) - end - - doc - end - # Ensure that a :project key exists in context # # Note that while the key might exist, its value could be nil! def validate needs :project end + + # Iterates over all and text() nodes in a document. + # + # Nodes are skipped whenever their ancestor is one of the nodes returned + # by `ignore_ancestor_query`. Link tags are not processed if they have a + # "gfm" class or the "href" attribute is empty. + def each_node + query = %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})] + | descendant-or-self::a[ + not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "") + ]} + + doc.xpath(query).each do |node| + yield node + end + end + + # Yields the link's URL and text whenever the node is a valid tag. + def yield_valid_link(node) + link = CGI.unescape(node.attr('href').to_s) + text = node.text + + return unless link.force_encoding('UTF-8').valid_encoding? + + yield link, text + end + + def replace_text_when_pattern_matches(node, pattern) + return unless node.text =~ pattern + + content = node.to_html + html = yield content + + node.replace(html) unless content == html + end + + def replace_link_node_with_text(node, link) + html = yield + + node.replace(html) unless html == node.text + end + + def replace_link_node_with_href(node, link) + html = yield + + node.replace(html) unless html == link + end + + def text_node?(node) + node.is_a?(Nokogiri::XML::Text) + end + + def element_node?(node) + node.is_a?(Nokogiri::XML::Element) + end end end end diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb index 989fa64e07..eea3af842b 100644 --- a/lib/banzai/filter/user_reference_filter.rb +++ b/lib/banzai/filter/user_reference_filter.rb @@ -59,13 +59,28 @@ module Banzai end def call - replace_text_nodes_matching(User.reference_pattern) do |content| - user_link_filter(content) + return doc if project.nil? + + ref_pattern = User.reference_pattern + ref_pattern_start = /\A#{ref_pattern}\z/ + + each_node do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + user_link_filter(content) + end + elsif element_node?(node) + yield_valid_link(node) do |link, text| + if link =~ ref_pattern_start + replace_link_node_with_href(node, link) do + user_link_filter(link, link_text: text) + end + end + end + end end - replace_link_nodes_with_href(User.reference_pattern) do |link, text| - user_link_filter(link, link_text: text) - end + doc end # Replace `@user` user references in text with links to the referenced From 8c49eaa937ed3d4332c54e8b0929c328a85d7fe4 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Thu, 24 Mar 2016 16:27:52 +0100 Subject: [PATCH 366/618] Cache Banzai class methods returning static data These methods always return the same data for every class so there's no point in computing their values on every call. --- lib/banzai/filter/abstract_reference_filter.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 16b703a332..051b94f9ce 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -11,15 +11,15 @@ module Banzai end def self.object_name - object_class.name.underscore + @object_name ||= object_class.name.underscore end def self.object_sym - object_name.to_sym + @object_sym ||= object_name.to_sym end def self.data_reference - "data-#{object_name.dasherize}" + @data_reference ||= "data-#{object_name.dasherize}" end # Public: Find references in text (like `!123` for merge requests) From 9fa94326dba11ca3b9197a4f084ba2883c29bdff Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Tue, 29 Mar 2016 11:58:05 +0200 Subject: [PATCH 367/618] Memoize object class titles For an issue with around 200 notes this cuts down timings by around 150 milliseconds. --- lib/banzai/filter/abstract_reference_filter.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 051b94f9ce..02ef27cf57 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -22,6 +22,10 @@ module Banzai @data_reference ||= "data-#{object_name.dasherize}" end + def self.object_class_title + @object_title ||= object_class.name.titleize + end + # Public: Find references in text (like `!123` for merge requests) # # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches| @@ -53,6 +57,10 @@ module Banzai self.class.object_sym end + def object_class_title + self.class.object_class_title + end + def references_in(*args, &block) self.class.references_in(*args, &block) end @@ -206,7 +214,7 @@ module Banzai end def object_link_title(object) - "#{object_class.name.titleize}: #{object.title}" + "#{object_class_title}: #{object.title}" end def object_link_text(object, matches) From 7f0fd73eeb983782bac26bb983ec8ea52194b80a Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 30 Mar 2016 14:04:28 +0200 Subject: [PATCH 368/618] Cache default_issues_tracker? in Banzai Every object processed by ExternalIssueReferenceFilter can return a different Project instance when calling "project". For example, every note processed will have it's own associated Project. If we were to cache Project#default_issues_tracker? on Project level this would have no impact on Markdown rendering timings as the cache would have to be built for every Project instance without it ever being re-used. To work around this we cache Project#default_issues_tracker? in Banzai::Filter::ExternalIssueReferenceFilter using the project's _id_ instead of the whole object. This setup allows re-using of the cached data even when the Project instances used are different, as long as the actual project IDs are the same. --- .../filter/external_issue_reference_filter.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb index a3b66c4645..37344b9057 100644 --- a/lib/banzai/filter/external_issue_reference_filter.rb +++ b/lib/banzai/filter/external_issue_reference_filter.rb @@ -35,7 +35,7 @@ module Banzai def call # Early return if the project isn't using an external tracker - return doc if project.nil? || project.default_issues_tracker? + return doc if project.nil? || default_issues_tracker? ref_pattern = ExternalIssue.reference_pattern ref_start_pattern = /\A#{ref_pattern}\z/ @@ -90,6 +90,21 @@ module Banzai def url_for_issue(*args) IssuesHelper.url_for_issue(*args) end + + def default_issues_tracker? + if RequestStore.active? + default_issues_tracker_cache[project.id] ||= + project.default_issues_tracker? + else + project.default_issues_tracker? + end + end + + private + + def default_issues_tracker_cache + RequestStore[:banzai_default_issues_tracker_cache] ||= {} + end end end end From ede351a99e33d825b7c1421388d2cce2ca1278d5 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Wed, 30 Mar 2016 14:10:41 +0200 Subject: [PATCH 369/618] Added CHANGELOG entry for Markdown performance --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index fc5e06ed94..f8f21ed0dc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) - Don't attempt to fetch any tags from a forked repo (Stan Hu) + - Improved Markdown rendering performance !3389 (Yorick Peterse) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) From 915fd3f910921bc97abdda5f2ae63093c11533fd Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Apr 2016 11:39:11 +0200 Subject: [PATCH 370/618] Cleaned up caching in AbstractReferenceFilter Cleaning this up any further is a bit tricky as the caches in question should only be evaluated if RequestStore is actually enabled. --- .../filter/abstract_reference_filter.rb | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 02ef27cf57..f21dbef216 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -74,11 +74,7 @@ module Banzai if RequestStore.active? cache = find_objects_cache[object_class][project.id] - if cache.key?(id) - cache[id] - else - cache[id] = find_object(project, id) - end + get_or_set_cache(cache, id) { find_object(project, id) } else find_object(project, id) end @@ -88,11 +84,7 @@ module Banzai if RequestStore.active? cache = project_refs_cache - if cache.key?(ref) - cache[ref] - else - cache[ref] = project_from_ref(ref) - end + get_or_set_cache(cache, ref) { project_from_ref(ref) } else project_from_ref(ref) end @@ -107,11 +99,7 @@ module Banzai if RequestStore.active? cache = url_for_object_cache[object_class][project.id] - if cache.key?(object) - cache[object] - else - cache[object] = url_for_object(object, project) - end + get_or_set_cache(cache, object) { url_for_object(object, project) } else url_for_object(object, project) end @@ -243,6 +231,14 @@ module Banzai hash[key] = Hash.new { |h, k| h[k] = {} } end end + + def get_or_set_cache(cache, key) + if cache.key?(key) + cache[key] + else + cache[key] = yield + end + end end end end From b7f0b22b9fe76f634d9e8cbce03cfaac41f333e6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 29 Mar 2016 18:17:31 +0100 Subject: [PATCH 371/618] Started refactoring of note form --- app/assets/javascripts/notes.js.coffee | 9 ----- app/assets/stylesheets/framework/common.scss | 7 ---- app/assets/stylesheets/pages/note_form.scss | 7 ++-- app/views/projects/_zen.html.haml | 7 ++-- app/views/projects/notes/_form.html.haml | 2 +- .../projects/notes/_notes_with_form.html.haml | 34 +++++++++++-------- 6 files changed, 27 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ff06c57f2b..9963299988 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -251,14 +251,9 @@ class @Notes Sets some hidden fields in the form. ### setupMainTargetNoteForm: -> - # find the form form = $(".js-new-note-form") - # insert the form after the button - form.clone().replaceAll $(".js-main-target-form") - form = form.prev("form") - # show the form @setupNoteForm(form) @@ -266,10 +261,6 @@ class @Notes form.removeClass "js-new-note-form" form.addClass "js-main-target-form" - # remove unnecessary fields and buttons - form.find("#note_line_code").remove() - form.find(".js-close-discussion-note-form").remove() - ### General note form setup. diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 9b676d759e..91ac5af3c9 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -125,13 +125,6 @@ p.time { height: 150px; } -// Fixes alignment on notes. -.new_note { - label { - text-align: left; - } -} - // Fix issue with notes & lists creating a bunch of bottom borders. li.note { img { max-width: 100% } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 655f88b0c2..91b5216a22 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -17,16 +17,17 @@ } .diff-file, .discussion { - .new_note { + .new-note { margin: 0; border: none; } } -.new_note { + +.new-note { display: none; } -.new_note, .note-edit-form { +.new-note, .note-edit-form { .note-form-actions { margin-top: $gl-padding; } diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index e701253d7d..93a7e1cfeb 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -5,8 +5,7 @@ = f.text_area attr, class: classes - else = text_area_tag attr, nil, class: classes - %a.js-zen-enter(tabindex="-1" href="#") - = icon('expand') - Edit in fullscreen - %a.js-zen-leave(tabindex="-1" href="#") + %a.js-zen-enter{ tabindex: "-1", href: "#" } + Go full screen + %a.js-zen-leave{ tabindex: "-1", href: "#" } = icon('compress') diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index f675f092da..8517e5a765 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| += form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form gfm-form" }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 910eb6cf66..003f42db0b 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,20 +1,24 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" -.js-notes-busy - -.js-main-target-form -- if can? current_user, :create_note, @project - = render "projects/notes/form", view: diff_view -- else - .disabled-comment-area - .disabled-profile - .disabled-comment - %span - Please - = link_to "register",new_user_session_path - or - = link_to "login",new_user_session_path - to post a comment + %li.timeline-entry + .timeline-icon + - if can? current_user, :create_note, @project + %a.author_link{ href: user_path(current_user) } + = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' + .timeline-content + .js-main-target-form + - if can? current_user, :create_note, @project + = render "projects/notes/form", view: diff_view + - else + .disabled-comment-area + .disabled-profile + .disabled-comment + %span + Please + = link_to "register",new_user_session_path + or + = link_to "login",new_user_session_path + to post a comment :javascript var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") From 0331fa3f3d27dbffdd2144073cf9c62fe7837aa1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 08:28:06 +0100 Subject: [PATCH 372/618] Restyling on elements in comment form --- app/assets/javascripts/notes.js.coffee | 5 -- app/assets/javascripts/zen_mode.js.coffee | 2 +- .../stylesheets/framework/markdown_area.scss | 15 ++++++ .../stylesheets/framework/typography.scss | 2 +- app/assets/stylesheets/framework/zen.scss | 46 ++++++++----------- app/assets/stylesheets/pages/note_form.scss | 28 ++++++----- app/views/projects/_md_preview.html.haml | 18 ++++---- app/views/projects/_zen.html.haml | 8 ++-- app/views/projects/notes/_edit_form.html.haml | 2 +- app/views/projects/notes/_form.html.haml | 2 +- .../projects/notes/_notes_with_form.html.haml | 24 +++++----- 11 files changed, 76 insertions(+), 76 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 9963299988..24569c4f97 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,5 +1,4 @@ #= require autosave -#= require autosize #= require dropzone #= require dropzone_input #= require gfm_auto_complete @@ -288,7 +287,6 @@ class @Notes else previewButton.removeClass("turn-on").addClass "turn-off" - autosize(textarea) new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -370,9 +368,6 @@ class @Notes textarea = form.find("textarea") textarea.focus() - if isNewForm - autosize(textarea) - # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # The textarea has the correct value, Chrome just won't show it unless we # modify it, so let's clear it and re-set it! diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee index e1c5446eaa..99f35ecfb0 100644 --- a/app/assets/javascripts/zen_mode.js.coffee +++ b/app/assets/javascripts/zen_mode.js.coffee @@ -42,7 +42,7 @@ class @ZenMode $(e.currentTarget).trigger('zen_mode:leave') $(document).on 'zen_mode:enter', (e) => - @enter(e.target.parentNode) + @enter($(e.target).closest('.md-area').find('.zen-backdrop')) $(document).on 'zen_mode:leave', (e) => @exit() diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 8328aac4e7..9e2240bd21 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -65,6 +65,21 @@ position: relative; } +.md-header { + .nav-links { + .active { + a { + border-bottom-color: #000; + } + } + + a { + padding-top: 0; + line-height: 1; + } + } +} + .referenced-users { color: #4c4e54; padding-top: 10px; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index b1886fbe67..2ed0f82f91 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -244,7 +244,7 @@ a > code { * Textareas intended for GFM * */ -textarea.js-gfm-input { +.js-gfm-input { font-family: $monospace_font; color: $gl-text-color; } diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 02e24ec7c4..e75f4471e6 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -1,26 +1,4 @@ .zennable { - a.js-zen-enter { - color: $gl-gray; - position: absolute; - top: 0; - right: 4px; - line-height: 56px; - } - - a.js-zen-leave { - display: none; - color: $gl-text-color; - position: absolute; - top: 10px; - right: 10px; - padding: 5px; - font-size: 36px; - - &:hover { - color: #111; - } - } - .zen-backdrop { &.fullscreen { background-color: white; @@ -47,11 +25,7 @@ margin: 0 auto; } - a.js-zen-enter { - display: none; - } - - a.js-zen-leave { + .zen-control-leave { display: block; position: absolute; top: 0; @@ -59,3 +33,21 @@ } } } + +.zen-cotrol { + color: #555; + line-height: 31px; +} + +.zen-control-leave { + display: none; + color: $gl-text-color; + position: absolute; + right: 10px; + padding: 5px; + font-size: 36px; + + &:hover { + color: #111; + } +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 91b5216a22..554f87c05a 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -1,10 +1,6 @@ /** * Note Form */ - -.comment-btn { - @extend .btn-create; -} .reply-btn { @extend .btn-primary; margin: 10px $gl-padding; @@ -42,18 +38,22 @@ max-width: 100%; } - .note_text { - width: 100%; - } - .comment-hints { margin-top: -12px; } } -/* loading indicator */ -.notes-busy { - margin: 18px; +.note-textarea { + padding-left: 0; + padding-right: 0; + font-family: $regular_font; + border-left: 0; + border-right: 0; + resize: none!important; // TODO: Find a way to remove this !important + + &:focus { + outline: 0; + } } .note-image-attach { @@ -64,11 +64,9 @@ .common-note-form { margin: 0; - background: #fff; padding: $gl-padding; - margin-left: -$gl-padding; - margin-right: -$gl-padding; - margin-bottom: -$gl-padding; + border: 1px solid #E5E5E5; + border-radius: $border-radius-base; } .note-form-actions { diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 1fb37ef662..32c4433861 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,18 +1,20 @@ .md-area .md-header.clearfix - %ul.nav-links + %ul.nav-links.pull-left %li.active - %a.js-md-write-button(href="#md-write-holder" tabindex="-1") + %a.js-md-write-button{ href: "#md-write-holder" } Write %li - %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1") + %a.js-md-preview-button{ href: "#md-preview-holder" } Preview + .pull-right + %a.zen-cotrol.js-zen-enter{ href: "#" } + Go full screen - %div - .md-write-holder - = yield - .md.md-preview-holder.hide - .js-md-preview{class: (preview_class if defined?(preview_class))} + .md-write-holder + = yield + .md.md-preview-holder.hide + .js-md-preview{class: (preview_class if defined?(preview_class))} - if defined?(referenced_users) && referenced_users %div.referenced-users.hide diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 93a7e1cfeb..efa60c88b1 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -2,10 +2,8 @@ .zen-backdrop - classes << ' js-gfm-input js-autosize markdown-area' - if defined?(f) && f - = f.text_area attr, class: classes + = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." - else - = text_area_tag attr, nil, class: classes - %a.js-zen-enter{ tabindex: "-1", href: "#" } - Go full screen - %a.js-zen-leave{ tabindex: "-1", href: "#" } + = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." + %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } = icon('compress') diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 2999befffc..3a5551b08c 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -2,7 +2,7 @@ = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' = render 'projects/notes/hints' .note-form-actions.clearfix diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 8517e5a765..c446ecec2c 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -8,7 +8,7 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' + = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text' = render 'projects/notes/hints' .error-alert diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 003f42db0b..8a5a319995 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -7,18 +7,18 @@ = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' .timeline-content .js-main-target-form - - if can? current_user, :create_note, @project - = render "projects/notes/form", view: diff_view - - else - .disabled-comment-area - .disabled-profile - .disabled-comment - %span - Please - = link_to "register",new_user_session_path - or - = link_to "login",new_user_session_path - to post a comment + - if can? current_user, :create_note, @project + = render "projects/notes/form", view: diff_view + - else + .disabled-comment-area + .disabled-profile + .disabled-comment + %span + Please + = link_to "register",new_user_session_path + or + = link_to "login",new_user_session_path + to post a comment :javascript var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") From 3c2b0e7572c11d24b96f2762a03c8cc47f11f510 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 10:00:43 +0100 Subject: [PATCH 373/618] Added toolbar to comment form --- app/assets/javascripts/issue.js.coffee | 15 ---- .../javascripts/merge_request.js.coffee | 16 ---- app/assets/javascripts/notes.js.coffee | 6 ++ .../stylesheets/framework/markdown_area.scss | 12 +-- app/assets/stylesheets/framework/zen.scss | 73 +++++++++++-------- app/assets/stylesheets/pages/note_form.scss | 59 +++++++++++---- app/views/projects/_md_preview.html.haml | 13 ++-- app/views/projects/_zen.html.haml | 17 ++--- app/views/projects/notes/_hints.html.haml | 28 +++++-- 9 files changed, 130 insertions(+), 109 deletions(-) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index d663e34871..44a8aa6883 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -6,25 +6,10 @@ class @Issue constructor: -> # Prevent duplicate event bindings @disableTaskList() - @fixAffixScroll() if $('a.btn-close').length @initTaskList() @initIssueBtnEventListeners() - fixAffixScroll: -> - fixAffix = -> - $discussion = $('.issuable-discussion') - $sidebar = $('.issuable-sidebar') - if $sidebar.hasClass('no-affix') - $sidebar.removeClass(['affix-top','affix']) - discussionHeight = $discussion.height() - sidebarHeight = $sidebar.height() - if sidebarHeight > discussionHeight - $discussion.height(sidebarHeight + 50) - $sidebar.addClass('no-affix') - $(window).on('resize', fixAffix) - fixAffix() - initTaskList: -> $('.detail-page-description .js-task-list-container').taskList('enable') $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 6af5a48a0b..1f46e33142 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -15,8 +15,6 @@ class @MergeRequest this.$('.show-all-commits').on 'click', => this.showAllCommits() - @fixAffixScroll(); - @initTabs() # Prevent duplicate event bindings @@ -30,20 +28,6 @@ class @MergeRequest $: (selector) -> this.$el.find(selector) - fixAffixScroll: -> - fixAffix = -> - $discussion = $('.issuable-discussion') - $sidebar = $('.issuable-sidebar') - if $sidebar.hasClass('no-affix') - $sidebar.removeClass(['affix-top','affix']) - discussionHeight = $discussion.height() - sidebarHeight = $sidebar.height() - if sidebarHeight > discussionHeight - $discussion.height(sidebarHeight + 50) - $sidebar.addClass('no-affix') - $(window).on('resize', fixAffix) - fixAffix() - initTabs: -> if @opts.action != 'new' # `MergeRequests#new` has no tab-persisting or lazy-loading behavior diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 24569c4f97..864156cb71 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -1,4 +1,5 @@ #= require autosave +#= require autosize #= require dropzone #= require dropzone_input #= require gfm_auto_complete @@ -287,6 +288,8 @@ class @Notes else previewButton.removeClass("turn-on").addClass "turn-off" + autosize(textarea) + new Autosave textarea, [ "Note" form.find("#note_commit_id").val() @@ -368,6 +371,9 @@ class @Notes textarea = form.find("textarea") textarea.focus() + if isNewForm + autosize(textarea) + # HACK (rspeicher/DouweM): Work around a Chrome 43 bug(?). # The textarea has the correct value, Chrome just won't show it unless we # modify it, so let's clear it and re-set it! diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 9e2240bd21..ae23e19172 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -1,9 +1,7 @@ .div-dropzone-wrapper { .div-dropzone { position: relative; - padding: 0; - border: 0; - margin-bottom: 5px; + margin-bottom: -5px; .div-dropzone-focus { border-color: #66afe9 !important; @@ -25,12 +23,10 @@ .div-dropzone-spinner { position: absolute; - top: 100%; - left: 100%; - margin-top: -1.1em; - margin-left: -1.1em; + bottom: 10px; + right: 5px; opacity: 0; - font-size: 30px; + font-size: 20px; transition: opacity 200ms ease-in-out; } diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index e75f4471e6..951d794916 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -1,42 +1,51 @@ -.zennable { - .zen-backdrop { - &.fullscreen { - background-color: white; - position: fixed; +.zen-backdrop { + &.fullscreen { + background-color: white; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 1031; + + textarea { + border: none; + box-shadow: none; + border-radius: 0; + color: #000; + font-size: 20px; + line-height: 26px; + padding: 30px; + display: block; + outline: none; + resize: none; + height: 100vh; + max-width: 900px; + margin: 0 auto; + } + + .zen-control-leave { + display: block; + position: absolute; top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 1031; - - textarea { - border: none; - box-shadow: none; - border-radius: 0; - color: #000; - font-size: 20px; - line-height: 26px; - padding: 30px; - display: block; - outline: none; - resize: none; - height: 100vh; - max-width: 900px; - margin: 0 auto; - } - - .zen-control-leave { - display: block; - position: absolute; - top: 0; - } } } } .zen-cotrol { + padding: 0; color: #555; - line-height: 31px; + background: none; + border: 0; +} + +.zen-control-full { + color: #959494; + + &:hover { + color: $gl-link-color; + text-decoration: none; + } } .zen-control-leave { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 554f87c05a..561b37e582 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -47,9 +47,7 @@ padding-left: 0; padding-right: 0; font-family: $regular_font; - border-left: 0; - border-right: 0; - resize: none!important; // TODO: Find a way to remove this !important + border: 0; &:focus { outline: 0; @@ -63,10 +61,11 @@ } .common-note-form { - margin: 0; - padding: $gl-padding; - border: 1px solid #E5E5E5; - border-radius: $border-radius-base; + .md-area { + padding: $gl-padding-top $gl-padding; + border: 1px solid #E5E5E5; + border-radius: $border-radius-base; + } } .note-form-actions { @@ -151,11 +150,43 @@ } } -.comment-hints { - color: #999; - background: #fff; - padding: 7px; - margin-top: -7px; - border: 1px solid $border-color; - font-size: 13px; +.comment-toolbar { + padding-top: $gl-padding-top; + border-top: 1px solid $border-color; +} + +.toolbar-button { + float: left; + margin-right: $gl-padding; + padding: 0; + background: none; + border: 0; + color: #959494; + font-size: 14px; + line-height: 16px; + + &:hover, + &:focus { + color: $gl-link-color; + outline: 0; + } + + &:last-child { + margin-right: 0; + } +} + +.toolbar-button-icon { + position: relative; + top: 1px; + margin-right: 3px; + color: inherit; + font-size: 16px; +} + +.toolbar-text { + float: left; + color: #959494; + font-size: 14px; + line-height: 16px; } diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 32c4433861..4920910fee 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -1,20 +1,19 @@ .md-area - .md-header.clearfix - %ul.nav-links.pull-left + .md-header + %ul.nav-links %li.active %a.js-md-write-button{ href: "#md-write-holder" } Write %li %a.js-md-preview-button{ href: "#md-preview-holder" } Preview - .pull-right - %a.zen-cotrol.js-zen-enter{ href: "#" } - Go full screen + %li.pull-right + %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button' } + Go full screen .md-write-holder = yield - .md.md-preview-holder.hide - .js-md-preview{class: (preview_class if defined?(preview_class))} + .md.md-preview-holder.js-md-preview.hide{class: (preview_class if defined?(preview_class))} - if defined?(referenced_users) && referenced_users %div.referenced-users.hide diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index efa60c88b1..bddff5cdcb 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -1,9 +1,8 @@ -.zennable - .zen-backdrop - - classes << ' js-gfm-input js-autosize markdown-area' - - if defined?(f) && f - = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." - - else - = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." - %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } - = icon('compress') +.zen-backdrop + - classes << ' js-gfm-input js-autosize markdown-area' + - if defined?(f) && f + = f.text_area attr, class: classes, placeholder: "Write a comment or drag your files here..." + - else + = text_area_tag attr, nil, class: classes, placeholder: "Write a comment or drag your files here..." + %a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } + = icon('compress') diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 6e7929bdab..7f83656146 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -1,9 +1,21 @@ -.comment-hints.clearfix - .pull-left +.comment-toolbar.clearfix + %button.toolbar-button.js-toolbar-button{ type: 'button', data: { prefix: ':' }, tabindex: '-1' } + = icon('smile-o', class: 'toolbar-button-icon') + Emoji + .toolbar-text + Styling with = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 - tip: - = random_markdown_tip - .pull-right - = link_to '#', class: 'markdown-selector', tabindex: -1 do - = icon('paperclip') - Attach a file + is supported + %button.toolbar-button.markdown-selector.pull-right{ type: 'button', tabindex: '-1' } + = icon('file-image-o', class: 'toolbar-button-icon') + Attach a file + +-# .comment-hints.clearfix +-# .pull-left +-# = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 +-# tip: +-# = random_markdown_tip +-# .pull-right +-# = link_to '#', class: 'markdown-selector', tabindex: -1 do +-# = icon('paperclip') +-# Attach a file From 38e32780a85463e862e5847a436be6819e28c81c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 10:04:21 +0100 Subject: [PATCH 374/618] Hides current user icon on mobile --- app/assets/stylesheets/pages/notes.scss | 6 ++++++ app/views/projects/notes/_notes_with_form.html.haml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 072de68c8e..0f45c1fe33 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -20,6 +20,12 @@ ul.notes { .timeline-content { margin-left: 55px; + + &.timeline-content-form { + @media (max-width: $screen-sm-max) { + margin-left: 0; + } + } } .note-created-ago, .note-updated-at { diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 8a5a319995..51e33dd774 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,11 +1,11 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" %li.timeline-entry - .timeline-icon + .timeline-icon.hidden-xs.hidden-sm - if can? current_user, :create_note, @project %a.author_link{ href: user_path(current_user) } = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' - .timeline-content + .timeline-content.timeline-content-form .js-main-target-form - if can? current_user, :create_note, @project = render "projects/notes/form", view: diff_view From af3284d98b68dedbdab3821fa99ee53437ee3d32 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 10:19:58 +0100 Subject: [PATCH 375/618] Focus style for comment form --- app/assets/javascripts/notes.js.coffee | 6 ++++ .../stylesheets/framework/markdown_area.scss | 7 ++-- app/assets/stylesheets/pages/note_form.scss | 32 ++++++++++++++----- app/views/projects/notes/_hints.html.haml | 2 +- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 864156cb71..40326ec96a 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -288,6 +288,12 @@ class @Notes else previewButton.removeClass("turn-on").addClass "turn-off" + textarea.on 'focus', -> + $(this).closest('.md-area').addClass 'is-focused' + + textarea.on 'blur', -> + $(this).closest('.md-area').removeClass 'is-focused' + autosize(textarea) new Autosave textarea, [ diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index ae23e19172..ea8e1c902c 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -82,11 +82,8 @@ } .md-preview-holder { - background: #fff; - border: 1px solid #ddd; - min-height: 169px; - padding: 5px; - box-shadow: none; + min-height: 167px; + padding: 10px 0; } .markdown-area { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 561b37e582..7bd666d1c6 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -44,8 +44,7 @@ } .note-textarea { - padding-left: 0; - padding-right: 0; + padding: 10px 0; font-family: $regular_font; border: 0; @@ -63,8 +62,18 @@ .common-note-form { .md-area { padding: $gl-padding-top $gl-padding; - border: 1px solid #E5E5E5; + border: 1px solid #e5e5e5; border-radius: $border-radius-base; + + &.is-focused { + border-color: #3b99fc; + box-shadow: 0 0 4px rgba(#3b99fc, .7); + + .comment-toolbar, + .nav-links { + border-color: #3b99fc; + } + } } } @@ -156,8 +165,6 @@ } .toolbar-button { - float: left; - margin-right: $gl-padding; padding: 0; background: none; border: 0; @@ -171,8 +178,14 @@ outline: 0; } - &:last-child { - margin-right: 0; + @media (min-width: $screen-md-min) { + float: left; + margin-right: $gl-padding; + + &:last-child { + float: right; + margin-right: 0; + } } } @@ -185,8 +198,11 @@ } .toolbar-text { - float: left; color: #959494; font-size: 14px; line-height: 16px; + + @media (min-width: $screen-md-min) { + float: left; + } } diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 7f83656146..19783c8a81 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -6,7 +6,7 @@ Styling with = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 is supported - %button.toolbar-button.markdown-selector.pull-right{ type: 'button', tabindex: '-1' } + %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } = icon('file-image-o', class: 'toolbar-button-icon') Attach a file From f0d2f370ccef7fd1fca4477111e6ded8133c4b36 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 10:24:50 +0100 Subject: [PATCH 376/618] Removed css code that isnt used --- app/assets/javascripts/notes.js.coffee | 1 - app/assets/stylesheets/pages/note_form.scss | 28 --------------------- app/views/projects/notes/_hints.html.haml | 10 -------- 3 files changed, 39 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 40326ec96a..ae16769ad4 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -305,7 +305,6 @@ class @Notes ] # remove notify commit author checkbox for non-commit notes - form.find(".js-notify-commit-author").remove() if form.find("#note_noteable_type").val() isnt "Commit" GitLab.GfmAutoComplete.setup() new DropzoneInput(form) form.show() diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 7bd666d1c6..5691279121 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -37,10 +37,6 @@ img { max-width: 100%; } - - .comment-hints { - margin-top: -12px; - } } .note-textarea { @@ -77,30 +73,6 @@ } } -.note-form-actions { - .note-form-option { - margin-top: 8px; - margin-left: 30px; - @extend .pull-left; - } - - .js-notify-commit-author { - float: left; - } - - .write-preview-btn { - // makes the "absolute" position for links relative to this - position: relative; - - // preview/edit buttons - > a { - position: absolute; - right: 5px; - top: 8px; - } - } -} - .note-edit-form { display: none; font-size: 15px; diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 19783c8a81..0c6758210b 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -9,13 +9,3 @@ %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } = icon('file-image-o', class: 'toolbar-button-icon') Attach a file - --# .comment-hints.clearfix --# .pull-left --# = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 --# tip: --# = random_markdown_tip --# .pull-right --# = link_to '#', class: 'markdown-selector', tabindex: -1 do --# = icon('paperclip') --# Attach a file From 0581df2971d63bcb7966637b6a3da2b4d7feb62d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 13:39:41 +0100 Subject: [PATCH 377/618] Discussion form --- app/assets/javascripts/notes.js.coffee | 13 +++++++--- app/assets/stylesheets/pages/note_form.scss | 5 ++++ .../projects/notes/_notes_with_form.html.haml | 26 +++++++++---------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ae16769ad4..22cad1d612 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -254,6 +254,9 @@ class @Notes # find the form form = $(".js-new-note-form") + # Set a global clone of the form for later cloning + @formClone = form.clone() + # show the form @setupNoteForm(form) @@ -452,15 +455,15 @@ class @Notes Shows the note form below the notes. ### replyToDiscussionNote: (e) => - form = $(".js-new-note-form") + form = @formClone.clone() replyLink = $(e.target).closest(".js-discussion-reply-button") replyLink.hide() # insert the form after the button - form.clone().insertAfter replyLink + replyLink.after form # show the form - @setupDiscussionNoteForm(replyLink, replyLink.next("form")) + @setupDiscussionNoteForm(replyLink, form) ### Shows the diff or discussion form and does some setup on it. @@ -485,7 +488,9 @@ class @Notes .text(form.find('.js-close-discussion-note-form').data('cancel-text')) @setupNoteForm form form.find(".js-note-text").focus() - form.addClass "js-discussion-note-form" + form + .removeClass('js-main-target-form') + .addClass("discussion-form js-discussion-note-form") ### Called when clicking on the "add a comment" button on the side of a diff line. diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 5691279121..85b323e2ca 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -73,6 +73,11 @@ } } +.discussion-form { + padding: $gl-padding-top $gl-padding; + background-color: #fff; +} + .note-edit-form { display: none; font-size: 15px; diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 51e33dd774..2f9903e26f 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,24 +1,24 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" +%ul.notes.timeline %li.timeline-entry .timeline-icon.hidden-xs.hidden-sm - if can? current_user, :create_note, @project %a.author_link{ href: user_path(current_user) } = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' .timeline-content.timeline-content-form - .js-main-target-form - - if can? current_user, :create_note, @project - = render "projects/notes/form", view: diff_view - - else - .disabled-comment-area - .disabled-profile - .disabled-comment - %span - Please - = link_to "register",new_user_session_path - or - = link_to "login",new_user_session_path - to post a comment + - if can? current_user, :create_note, @project + = render "projects/notes/form", view: diff_view + - else + .disabled-comment-area + .disabled-profile + .disabled-comment + %span + Please + = link_to "register",new_user_session_path + or + = link_to "login",new_user_session_path + to post a comment :javascript var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") From 3b4c4dd7b35b662755fed58c6d1ed6f9ba82b575 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 13:54:35 +0100 Subject: [PATCH 378/618] Logged out box --- app/assets/javascripts/notes.js.coffee | 3 +- .../stylesheets/pages/merge_requests.scss | 42 ++++++------------- app/views/projects/notes/_edit_form.html.haml | 5 ++- .../projects/notes/_notes_with_form.html.haml | 25 +++++------ 4 files changed, 27 insertions(+), 48 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 22cad1d612..0ee1e70da2 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -501,7 +501,6 @@ class @Notes addDiffNote: (e) => e.preventDefault() link = e.currentTarget - form = $(".js-new-note-form") row = $(link).closest("tr") nextRow = row.next() hasNotes = nextRow.is(".notes_holder") @@ -533,7 +532,7 @@ class @Notes addForm = true if addForm - newForm = form.clone() + newForm = @formClone.clone() newForm.appendTo row.next().find(targetContent) # show the form diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 7ff63ca20b..b080490f75 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -195,39 +195,21 @@ line-height: 31px; } -.disabled-comment-area { - padding: 16px 0; +.disabled-comment { + margin-left: -$gl-padding-top; + margin-right: -$gl-padding-top; + background-color: $gray-light; + border-radius: $border-radius-base; + border: 1px solid $border-gray-normal; + color: #b2b2b2; + line-height: 200px; - .disabled-profile { - width: 40px; - height: 40px; - background: $border-gray-dark; - border-radius: 20px; - display: inline-block; - margin-right: 10px; + .disabled-comment-text { + line-height: normal; } - .disabled-comment { - background: $gray-light; - display: inline-block; - vertical-align: top; - height: 200px; - border-radius: 4px; - border: 1px solid $border-gray-normal; - padding-top: 90px; - text-align: center; - right: 20px; - position: absolute; - left: 70px; - margin-bottom: 20px; - - span { - color: #b2b2b2; - - a { - color: $md-link-color; - } - } + a { + color: $gl-link-color; } } diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 3a5551b08c..23e4f93eab 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,5 +1,5 @@ .note-edit-form - = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note js-quick-submit' } do |f| + = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field' @@ -7,4 +7,5 @@ .note-form-actions.clearfix = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button' - = link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel' + %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } + Cancel diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 2f9903e26f..cc42aab5c5 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -2,23 +2,20 @@ = render "projects/notes/notes" %ul.notes.timeline %li.timeline-entry - .timeline-icon.hidden-xs.hidden-sm - - if can? current_user, :create_note, @project + - if can? current_user, :create_note, @project + .timeline-icon.hidden-xs.hidden-sm %a.author_link{ href: user_path(current_user) } = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' - .timeline-content.timeline-content-form - - if can? current_user, :create_note, @project + .timeline-content.timeline-content-form = render "projects/notes/form", view: diff_view - - else - .disabled-comment-area - .disabled-profile - .disabled-comment - %span - Please - = link_to "register",new_user_session_path - or - = link_to "login",new_user_session_path - to post a comment + - else + .disabled-comment.text-center + .disabled-comment-text.inline + Please + = link_to "register",new_user_session_path + or + = link_to "login",new_user_session_path + to post a comment :javascript var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") From 40297b8f118742fc710f2ae0364d7bbcd82c40e6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 14:03:14 +0100 Subject: [PATCH 379/618] Reduced focus shadow --- app/assets/stylesheets/pages/note_form.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 85b323e2ca..c829ffa330 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -63,7 +63,8 @@ &.is-focused { border-color: #3b99fc; - box-shadow: 0 0 4px rgba(#3b99fc, .7); + box-shadow: 0 0 2px rgba(#000, .2), + 0 0 4px rgba(#3b99fc, .5); .comment-toolbar, .nav-links { From 29f414aa5a4d63d5676d5c0e4d89584f45ef8c35 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 16:20:43 +0100 Subject: [PATCH 380/618] Tests update --- app/assets/stylesheets/pages/notes.scss | 2 +- features/steps/shared/diff_note.rb | 4 ++-- spec/features/issues_spec.rb | 2 +- spec/features/notes_on_merge_requests_spec.rb | 2 +- spec/javascripts/fixtures/zen_mode.html.haml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 0f45c1fe33..46a0724603 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -155,7 +155,7 @@ ul.notes { &.notes_content { background-color: #fff; border-width: 1px 0; - padding-top: 0; + padding: 0; vertical-align: top; &.parallel{ border-width: 1px; diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 906b66a4a6..32c3e99f45 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -125,7 +125,7 @@ module SharedDiffNote step 'I should only see one diff form' do page.within(diff_file_selector) do - expect(page).to have_css("form.new_note", count: 1) + expect(page).to have_css("form.new-note", count: 1) end end @@ -161,7 +161,7 @@ module SharedDiffNote step 'I should see a temporary diff comment form' do page.within(diff_file_selector) do - expect(page).to have_css(".js-temp-notes-holder form.new_note") + expect(page).to have_css(".js-temp-notes-holder form.new-note") end end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index db46657c36..79000666cc 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -22,7 +22,7 @@ describe 'Issues', feature: true do before do visit edit_namespace_project_issue_path(project.namespace, project, issue) - click_link "Edit" + click_button "Go full screen" end it 'should open new issue popup' do diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index d9a8058efd..133fca6923 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -152,7 +152,7 @@ describe 'Comments', feature: true do it 'has .new_note css class' do page.within('.js-temp-notes-holder') do - expect(subject).to have_css('.new_note') + expect(subject).to have_css('.new-note') end end end diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml index 1701652c61..cb906a7fea 100644 --- a/spec/javascripts/fixtures/zen_mode.html.haml +++ b/spec/javascripts/fixtures/zen_mode.html.haml @@ -1,4 +1,4 @@ -.zennable +.md-area .zen-backdrop %textarea#note_note.js-gfm-input.markdown-area %a.js-zen-enter(tabindex="-1" href="#") From ccc64676a97d251658190bfb62e97b166cee4db1 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 17:13:18 +0100 Subject: [PATCH 381/618] Commits comment test update --- features/steps/shared/note.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index fb0462d6e0..7949d252f0 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -2,7 +2,7 @@ module SharedNote include Spinach::DSL step 'I delete a comment' do - page.within('.notes') do + page.within('.main-notes-list') do find('.note').hover find(".js-note-delete").click end From 1f5083343081adb6e4a9a438600163844d2e9875 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 30 Mar 2016 20:11:41 +0100 Subject: [PATCH 382/618] Updated tests --- app/assets/javascripts/notes.js.coffee | 14 +++++++++----- features/steps/shared/note.rb | 2 +- spec/features/notes_on_merge_requests_spec.rb | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 0ee1e70da2..35547adf8a 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -65,6 +65,8 @@ class @Notes # add diff note $(document).on "click", ".js-add-diff-note-button", @addDiffNote + $(document).on "mouseover", ".js-add-diff-note-button", -> + console.log $(this).data('line-code') # hide diff note form $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm @@ -264,6 +266,8 @@ class @Notes form.removeClass "js-new-note-form" form.addClass "js-main-target-form" + form.find("#note_line_code").remove() + ### General note form setup. @@ -500,8 +504,9 @@ class @Notes ### addDiffNote: (e) => e.preventDefault() - link = e.currentTarget - row = $(link).closest("tr") + $link = $(e.currentTarget) + console.log $link.data('line-code') + row = $link.closest("tr") nextRow = row.next() hasNotes = nextRow.is(".notes_holder") addForm = false @@ -510,7 +515,7 @@ class @Notes # In parallel view, look inside the correct left/right pane if @isParallelView() - lineType = $(link).data("lineType") + lineType = $link.data("lineType") targetContent += "." + lineType rowCssToAdd = "" @@ -536,7 +541,7 @@ class @Notes newForm.appendTo row.next().find(targetContent) # show the form - @setupDiscussionNoteForm $(link), newForm + @setupDiscussionNoteForm $link, newForm ### Called in response to "cancel" on a diff note form. @@ -561,7 +566,6 @@ class @Notes cancelDiscussionForm: (e) => e.preventDefault() - form = $(".js-new-note-form") form = $(e.target).closest(".js-discussion-note-form") @removeDiscussionNoteForm(form) diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 7949d252f0..a3c3887ab4 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -128,7 +128,7 @@ module SharedNote end step 'I edit the last comment with a +1' do - page.within(".notes") do + page.within(".main-notes-list") do find(".note").hover find('.js-note-edit').click end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index 133fca6923..70d0864783 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -225,6 +225,6 @@ describe 'Comments', feature: true do end def click_diff_line(data = line_code) - page.find(%Q{button[data-line-code="#{data}"]}, visible: false).click + execute_script("$('button[data-line-code=\"#{data}\"]').click()") end end From f62d7d261b6d321400a68fa23daf7eceaba66e05 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 4 Apr 2016 10:49:08 +0100 Subject: [PATCH 383/618] Removed console.log from notes --- app/assets/javascripts/notes.js.coffee | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 35547adf8a..86e3b860fc 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -65,8 +65,6 @@ class @Notes # add diff note $(document).on "click", ".js-add-diff-note-button", @addDiffNote - $(document).on "mouseover", ".js-add-diff-note-button", -> - console.log $(this).data('line-code') # hide diff note form $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm @@ -505,7 +503,6 @@ class @Notes addDiffNote: (e) => e.preventDefault() $link = $(e.currentTarget) - console.log $link.data('line-code') row = $link.closest("tr") nextRow = row.next() hasNotes = nextRow.is(".notes_holder") From 51df93607fc5c4ef2cac13a90aaf7450c38fdf4f Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 4 Apr 2016 11:00:29 +0100 Subject: [PATCH 384/618] SCSS colors into variables --- .../stylesheets/framework/variables.scss | 6 ++++++ app/assets/stylesheets/framework/zen.scss | 4 ++-- .../stylesheets/pages/merge_requests.scss | 18 ------------------ app/assets/stylesheets/pages/note_form.scss | 11 +++++------ app/assets/stylesheets/pages/notes.scss | 18 ++++++++++++++++++ 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 98fe794d36..c2defd3188 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -217,3 +217,9 @@ $notes-light-color: #8e8e8e; $notes-action-color: #c3c3c3; $notes-role-color: #8e8e8e; $notes-role-border-color: #e4e4e4; + +$note-disabled-comment-color: #b2b2b2; +$note-form-border-color: #e5e5e5; +$note-toolbar-color: #959494; + +$zen-control-hover-color: #111; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 951d794916..f870ea0d87 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -40,7 +40,7 @@ } .zen-control-full { - color: #959494; + color: $note-toolbar-color; &:hover { color: $gl-link-color; @@ -57,6 +57,6 @@ font-size: 36px; &:hover { - color: #111; + color: $zen-control-hover-color; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index b080490f75..1c6a420897 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -195,24 +195,6 @@ line-height: 31px; } -.disabled-comment { - margin-left: -$gl-padding-top; - margin-right: -$gl-padding-top; - background-color: $gray-light; - border-radius: $border-radius-base; - border: 1px solid $border-gray-normal; - color: #b2b2b2; - line-height: 200px; - - .disabled-comment-text { - line-height: normal; - } - - a { - color: $gl-link-color; - } -} - .builds { .table-holder { overflow-x: scroll; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index c829ffa330..a909776b43 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -58,17 +58,17 @@ .common-note-form { .md-area { padding: $gl-padding-top $gl-padding; - border: 1px solid #e5e5e5; + border: 1px solid $note-form-border-color; border-radius: $border-radius-base; &.is-focused { - border-color: #3b99fc; + border-color: $focus-border-color; box-shadow: 0 0 2px rgba(#000, .2), - 0 0 4px rgba(#3b99fc, .5); + 0 0 4px rgba($focus-border-color, .4); .comment-toolbar, .nav-links { - border-color: #3b99fc; + border-color: $focus-border-color; } } } @@ -139,6 +139,7 @@ .comment-toolbar { padding-top: $gl-padding-top; + color: $note-toolbar-color; border-top: 1px solid $border-color; } @@ -146,7 +147,6 @@ padding: 0; background: none; border: 0; - color: #959494; font-size: 14px; line-height: 16px; @@ -176,7 +176,6 @@ } .toolbar-text { - color: #959494; font-size: 14px; line-height: 16px; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 46a0724603..a9f88d2ccf 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -287,3 +287,21 @@ ul.notes { } } } + +.disabled-comment { + margin-left: -$gl-padding-top; + margin-right: -$gl-padding-top; + background-color: $gray-light; + border-radius: $border-radius-base; + border: 1px solid $border-gray-normal; + color: $note-disabled-comment-color; + line-height: 200px; + + .disabled-comment-text { + line-height: normal; + } + + a { + color: $gl-link-color; + } +} From 787713895851a7260e2c46e2863b1bbb68b3a649 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sun, 3 Apr 2016 22:50:53 -0700 Subject: [PATCH 385/618] Fix creation of merge requests for orphaned branches Closes #14875 --- CHANGELOG | 1 + app/views/projects/diffs/_image.html.haml | 7 +++-- .../merge_requests/create_new_mr_spec.rb | 28 +++++++++++++++++++ spec/support/test_env.rb | 1 + 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 spec/features/merge_requests/create_new_mr_spec.rb diff --git a/CHANGELOG b/CHANGELOG index fc5e06ed94..c661e1c3da 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ v 8.7.0 (unreleased) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Gracefully handle notes on deleted commits in merge requests (Stan Hu) + - Fix creation of merge requests for orphaned branches (Stan Hu) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 8367112a9c..2731219cca 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,7 +1,10 @@ - diff = diff_file.diff - file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) -- old_commit_id = diff_refs.first.id -- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) +// diff_refs will be nil for orphaned commits (e.g. first commit in repo) +- if diff_refs + - old_commit_id = diff_refs.first.id + - old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) + - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb new file mode 100644 index 0000000000..fd02d58484 --- /dev/null +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +feature 'Create New Merge Request', feature: true, js: false do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + before do + project.team << [user, :master] + + login_as user + visit namespace_project_merge_requests_path(project.namespace, project) + end + + it 'generates a diff for an orphaned branch' do + click_link 'New Merge Request' + select "orphaned-branch", from: "merge_request_source_branch" + select "master", from: "merge_request_target_branch" + click_button "Compare branches" + + expect(page).to have_content "README.md" + expect(page).to have_content "wm.png" + + fill_in "merge_request_title", with: "Orphaned MR test" + click_button "Submit merge request" + + expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 0d1bd030f3..71664bb192 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -15,6 +15,7 @@ module TestEnv 'lfs' => 'be93687', 'master' => '5937ac0', "'test'" => 'e56497b', + 'orphaned-branch' => '45127a9', } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily From 6ddfd49669207124466613ea0acce17bff50613b Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Apr 2016 14:17:19 +0200 Subject: [PATCH 386/618] Added 8.7 install/update guides [ci skip] --- doc/install/installation.md | 4 +- doc/update/8.6-to-8.7.md | 146 ++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 doc/update/8.6-to-8.7.md diff --git a/doc/install/installation.md b/doc/install/installation.md index bffbc77650..e0a16df09c 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -227,9 +227,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-6-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-7-stable gitlab -**Note:** You can change `8-6-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `8-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md new file mode 100644 index 0000000000..76eee147c7 --- /dev/null +++ b/doc/update/8.6-to-8.7.md @@ -0,0 +1,146 @@ +# From 8.6 to 8.7 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 8-7-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 8-7-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch --all +sudo -u git -H git checkout v2.7.0 +``` + +### 5. Update gitlab-workhorse + +Install and compile gitlab-workhorse. This requires +[Go 1.5](https://golang.org/dl) which should already be on your system from +GitLab 8.1. + +```bash +cd /home/git/gitlab-workhorse +sudo -u git -H git fetch --all +sudo -u git -H git checkout v0.7.1 +sudo -u git -H make +``` + +### 6. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +``` + +### 7. Update configuration files + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +# For HTTPS configurations +git diff origin/8-6-stable:lib/support/nginx/gitlab-ssl origin/8-7-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/8-6-stable:lib/support/nginx/gitlab origin/8-7-stable:lib/support/nginx/gitlab +``` + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-7-stable/lib/support/init.d/gitlab.default.example#L37 + +#### Init script + +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. Start application + + sudo service gitlab start + sudo service nginx restart + +### 9. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (8.6) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 8.5 to 8.6](8.5-to-8.6.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. From e014fe0426469577dff9a925ba079db27a87c54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Mon, 4 Apr 2016 15:27:58 +0200 Subject: [PATCH 387/618] Add 8.6.4 changelog item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- CHANGELOG | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 26e81f3c30..34cf78f8f8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,6 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) - - Don't attempt to fetch any tags from a forked repo (Stan Hu) - Improved Markdown rendering performance !3389 (Yorick Peterse) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Preserve time notes/comments have been updated at when moving issue @@ -20,6 +19,9 @@ v 8.7.0 (unreleased) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) +v 8.6.4 + - Don't attempt to fetch any tags from a forked repo (Stan Hu) + v 8.6.3 - Mentions on confidential issues doesn't create todos for non-members. !3374 - Destroy related todos when an Issue/MR is deleted. !3376 From a6b5b50e14885a82530794c6ea35c940305244dd Mon Sep 17 00:00:00 2001 From: Baldinof Date: Mon, 4 Apr 2016 14:41:01 +0000 Subject: [PATCH 388/618] Fix incorrect variable name --- app/services/projects/unlink_fork_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index d0703effa1..315c3e1629 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -4,7 +4,7 @@ module Projects return unless @project.forked? @project.forked_from_project.lfs_objects.find_each do |lfs_object| - lfs_object.projects << self + lfs_object.projects << @project end merge_requests = @project.forked_from_project.merge_requests.opened.from_project(@project) From 1be3c634c8372a4d10edc1ddf16fb50021624a57 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 4 Apr 2016 16:58:35 +0200 Subject: [PATCH 389/618] Fix transparent bg for expand sidebar button Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/sidebar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index c741c826ae..1d49249dd8 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -200,7 +200,7 @@ top: 0; left: 0; font-size: 20px; - background: transparent; + background: #fff; height: 59px; text-align: center; line-height: 59px; From 0163e27631fb993bd3541c09a95f0ef5e2026455 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 16 Mar 2016 18:10:03 +0100 Subject: [PATCH 390/618] Add Gitlab::Redis connection pool --- config/application.rb | 4 ++-- config/initializers/session_store.rb | 2 +- config/initializers/sidekiq.rb | 4 ++-- config/mail_room.yml | 4 ++-- lib/gitlab/exclusive_lease.rb | 4 +++- lib/gitlab/{redis_config.rb => redis.rb} | 11 ++++++++--- lib/tasks/cache.rake | 25 ++++++++++++------------ 7 files changed, 31 insertions(+), 23 deletions(-) rename lib/gitlab/{redis_config.rb => redis.rb} (72%) diff --git a/config/application.rb b/config/application.rb index 5a0ac70aa2..9633084d60 100644 --- a/config/application.rb +++ b/config/application.rb @@ -4,7 +4,7 @@ require 'rails/all' require 'devise' I18n.config.enforce_available_locales = false Bundler.require(:default, Rails.env) -require_relative '../lib/gitlab/redis_config' +require_relative '../lib/gitlab/redis' module Gitlab REDIS_CACHE_NAMESPACE = 'cache:gitlab' @@ -69,7 +69,7 @@ module Gitlab end end - redis_config_hash = Gitlab::RedisConfig.redis_store_options + redis_config_hash = Gitlab::Redis.redis_store_options redis_config_hash[:namespace] = REDIS_CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever config.cache_store = :redis_store, redis_config_hash diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 3da5d46be9..7028525587 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -13,7 +13,7 @@ end if Rails.env.test? Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" else - redis_config = Gitlab::RedisConfig.redis_store_options + redis_config = Gitlab::Redis.redis_store_options redis_config[:namespace] = 'session:gitlab' Gitlab::Application.config.session_store( diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index cc83137745..9182d92980 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -2,7 +2,7 @@ SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab' Sidekiq.configure_server do |config| config.redis = { - url: Gitlab::RedisConfig.url, + url: Gitlab::Redis.url, namespace: SIDEKIQ_REDIS_NAMESPACE } @@ -29,7 +29,7 @@ end Sidekiq.configure_client do |config| config.redis = { - url: Gitlab::RedisConfig.url, + url: Gitlab::Redis.url, namespace: SIDEKIQ_REDIS_NAMESPACE } end diff --git a/config/mail_room.yml b/config/mail_room.yml index 60257329f3..761a32adb9 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -2,7 +2,7 @@ <% require "yaml" require "json" -require_relative "lib/gitlab/redis_config" +require_relative "lib/gitlab/redis" rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" @@ -18,7 +18,7 @@ if File.exists?(config_file) config['mailbox'] = "inbox" if config['mailbox'].nil? if config['enabled'] && config['address'] - redis_url = Gitlab::RedisConfig.new(rails_env).url + redis_url = Gitlab::Redis.new(rails_env).url %> - :host: <%= config['host'].to_json %> diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb index c73eca832d..c2260a5f7a 100644 --- a/lib/gitlab/exclusive_lease.rb +++ b/lib/gitlab/exclusive_lease.rb @@ -43,7 +43,9 @@ module Gitlab # false if the lease is already taken. def try_obtain # Performing a single SET is atomic - !!redis.set(redis_key, '1', nx: true, ex: @timeout) + Gitlab::Redis.with do |redis| + !!redis.set(redis_key, '1', nx: true, ex: @timeout) + end end # No #cancel method. See comments above! diff --git a/lib/gitlab/redis_config.rb b/lib/gitlab/redis.rb similarity index 72% rename from lib/gitlab/redis_config.rb rename to lib/gitlab/redis.rb index 4949c6db53..d03e6e4cd9 100644 --- a/lib/gitlab/redis_config.rb +++ b/lib/gitlab/redis.rb @@ -1,14 +1,19 @@ module Gitlab - class RedisConfig + class Redis attr_reader :url def self.url - new.url + @url ||= new.url + end + + def self.with + @pool ||= ConnectionPool.new { ::Redis.new(url: url) } + @pool.with { |redis| yield redis } end def self.redis_store_options url = new.url - redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(url) + redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url) # Redis::Store does not handle Unix sockets well, so let's do it for them redis_uri = URI.parse(url) if redis_uri.scheme == 'unix' diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake index 51e746ef92..6c2e2e9149 100644 --- a/lib/tasks/cache.rake +++ b/lib/tasks/cache.rake @@ -4,18 +4,19 @@ namespace :cache do desc "GitLab | Clear redis cache" task :clear => :environment do - redis = Redis.new(url: Gitlab::RedisConfig.url) - cursor = REDIS_SCAN_START_STOP - loop do - cursor, keys = redis.scan( - cursor, - match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*", - count: CLEAR_BATCH_SIZE - ) - - redis.del(*keys) if keys.any? - - break if cursor == REDIS_SCAN_START_STOP + Gitlab::Redis.with do |redis| + cursor = REDIS_SCAN_START_STOP + loop do + cursor, keys = redis.scan( + cursor, + match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*", + count: CLEAR_BATCH_SIZE + ) + + redis.del(*keys) if keys.any? + + break if cursor == REDIS_SCAN_START_STOP + end end end end From 213ee62469c6518af8423f00fb902b7665d61204 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 18 Mar 2016 15:31:53 +0100 Subject: [PATCH 391/618] Be careful when setting class instance vars --- lib/gitlab/redis.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb index d03e6e4cd9..8c3aea2627 100644 --- a/lib/gitlab/redis.rb +++ b/lib/gitlab/redis.rb @@ -2,12 +2,23 @@ module Gitlab class Redis attr_reader :url + # To be thread-safe we must be careful when writing the class instance + # variables @url and @pool. Because @pool depends on @url we need two + # mutexes to prevent deadlock. + URL_MUTEX = Mutex.new + POOL_MUTEX = Mutex.new + private_constant :URL_MUTEX, :POOL_MUTEX + def self.url - @url ||= new.url + @url || URL_MUTEX.synchronize { @url = new.url } end def self.with - @pool ||= ConnectionPool.new { ::Redis.new(url: url) } + if @pool.nil? + POOL_MUTEX.synchronize do + @pool = ConnectionPool.new { ::Redis.new(url: url) } + end + end @pool.with { |redis| yield redis } end From 11c1dda35fdb3a058dda739270b1dac8f3516f16 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 4 Apr 2016 18:59:04 +0200 Subject: [PATCH 392/618] Fix event rendering when create project Signed-off-by: Dmitriy Zaporozhets --- app/helpers/events_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index a36b13a7db..592bad8ba2 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -216,7 +216,7 @@ module EventsHelper end def event_row_class(event) - if event.body? || event.created_project? + if event.body? "event-block" else "event-inline" From d5fadfe0f5ebc03d33d1e61bba89d054025870e8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 4 Apr 2016 19:23:50 +0200 Subject: [PATCH 393/618] Improve UI for admin/groups page Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/nav.scss | 11 ++++++ app/views/admin/groups/_group.html.haml | 28 ++++++++++++++ app/views/admin/groups/index.html.haml | 45 +++++------------------ 3 files changed, 49 insertions(+), 35 deletions(-) create mode 100644 app/views/admin/groups/_group.html.haml diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index fc3b0a422a..94f5a12ff6 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -56,6 +56,17 @@ } } + .nav-search { + display: inline-block; + width: 50%; + padding: 11px 0; + + /* Small devices (phones, tablets, 768px and lower) */ + @media (max-width: $screen-sm-min) { + width: 100%; + } + } + .nav-links { display: inline-block; width: 50%; diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml new file mode 100644 index 0000000000..166c0e9afe --- /dev/null +++ b/app/views/admin/groups/_group.html.haml @@ -0,0 +1,28 @@ +- css_class = '' unless local_assigns[:css_class] +- css_class += " no-description" if group.description.blank? + +%li.group-row{ class: css_class } + .controls.hidden-xs + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-grouped btn-sm" + = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-grouped btn-sm btn-remove" + + .stats + %span + = icon('bookmark') + = number_with_delimiter(group.projects.count) + + %span + = icon('users') + = number_with_delimiter(group.users.count) + + %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} + = visibility_level_icon(group.visibility_level, fw: false) + + = image_tag group_icon(group), class: "avatar s40 hidden-xs" + .title + = link_to [:admin, group], class: 'group-name' do + = group.name + + - if group.description.present? + .description + = markdown(group.description, pipeline: :description) diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 6bdc885a31..775072a744 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -1,20 +1,19 @@ - page_title "Groups" %h3.page-title Groups (#{number_with_delimiter(@groups.total_count)}) - = link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right" %p.light Group allows you to keep projects organized. Use groups for uniting related projects. -%hr -= form_tag admin_groups_path, method: :get, class: 'form-inline' do - = hidden_field_tag :sort, @sort - .form-group - = text_field_tag :name, params[:name], class: "form-control" - = button_tag "Search", class: "btn submit btn-primary" +.top-area + .nav-search + = form_tag admin_groups_path, method: :get, class: 'form-inline' do + = hidden_field_tag :sort, @sort + = text_field_tag :name, params[:name], class: "form-control" + = button_tag "Search", class: "btn submit btn-primary" - .pull-right + .nav-controls .dropdown.inline %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} %span.light @@ -33,34 +32,10 @@ = sort_title_recently_updated = link_to admin_groups_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated + = link_to 'New Group', new_admin_group_path, class: "btn btn-new" -%hr - -%ul.bordered-list +%ul.content-list - @groups.each do |group| - %li - .clearfix - .pull-right.prepend-top-10 - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-sm" - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-sm btn-remove" - - %h4 - = link_to [:admin, group] do - %span{ class: visibility_level_color(group.visibility_level) } - = visibility_level_icon(group.visibility_level) - - %i.fa.fa-folder - = group.name - - → - %span.monospace - %strong #{group.path}/ - .clearfix - %p - = truncate group.description, length: 150 - .clearfix - %p.light - #{pluralize(group.members.size, 'member')}, #{pluralize(group.projects.count, 'project')} - + = render 'group', group: group = paginate @groups, theme: "gitlab" From 3e6d27ee59d5750cc6c6140cda4fa08100dbd58a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 4 Apr 2016 19:36:21 +0200 Subject: [PATCH 394/618] Dont use monospace font for names Signed-off-by: Dmitriy Zaporozhets --- app/views/admin/builds/_build.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 588ad76742..3571eefd57 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -15,7 +15,7 @@ %td - if project - = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace" + = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project) %td = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace" From f9fd0031595be404c439899f5b68dfa753223fdd Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 4 Apr 2016 19:41:31 +0200 Subject: [PATCH 395/618] Fix missing paddings in admin area Signed-off-by: Dmitriy Zaporozhets --- app/views/admin/dashboard/index.html.haml | 2 +- app/views/admin/deploy_keys/index.html.haml | 2 +- app/views/admin/labels/index.html.haml | 12 +++++++----- app/views/admin/runners/index.html.haml | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 3274ba5377..6dd2fef395 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -1,4 +1,4 @@ -.admin-dashboard +.admin-dashboard.prepend-top-default .row .col-md-4 %h4 Statistics diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 41c4389997..149593e7f4 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -1,5 +1,5 @@ - page_title "Deploy Keys" -.panel.panel-default +.panel.panel-default.prepend-top-default .panel-heading Public deploy keys (#{@deploy_keys.count}) .controls diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml index 3c57e3dc17..05d6b9ed23 100644 --- a/app/views/admin/labels/index.html.haml +++ b/app/views/admin/labels/index.html.haml @@ -1,8 +1,10 @@ - page_title "Labels" -= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do - New label -%h3.page-title - Labels + +%div + = link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do + New label + %h3.page-title + Labels %hr .labels @@ -13,4 +15,4 @@ - else .light-well .nothing-here-block There are no labels yet - + diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index c407972cd0..2dad64b8d0 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,4 +1,4 @@ -%p.lead +%p.lead.prepend-top-default %span To register a new runner you should enter the following registration token. With this token the runner will request a unique runner token and use that for future communication. From 6e0f8453c0c1bef3dc4d9c1200aaf144011ce42d Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 4 Apr 2016 21:05:51 +0200 Subject: [PATCH 396/618] Reload forked_project_link projects This fixes ./spec/services/projects/unlink_fork_service_spec.rb which somehow started failing on the master branch. It certainly isn't a very elegant solution but seems to be the easiest/best way of solving this problem for the time being. --- spec/factories/forked_project_links.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb index 252bf2747e..19a54946fe 100644 --- a/spec/factories/forked_project_links.rb +++ b/spec/factories/forked_project_links.rb @@ -13,5 +13,10 @@ FactoryGirl.define do factory :forked_project_link do association :forked_to_project, factory: :project association :forked_from_project, factory: :project + + after(:create) do |link| + link.forked_from_project.reload + link.forked_to_project.reload + end end end From be6f1c74278519d49244a5c29b016725b8eec1e3 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 4 Apr 2016 23:52:27 +0200 Subject: [PATCH 397/618] Single quotes paradise Signed-off-by: Dmitriy Zaporozhets --- app/views/admin/groups/_group.html.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index 166c0e9afe..9025aaac09 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,10 +1,10 @@ - css_class = '' unless local_assigns[:css_class] -- css_class += " no-description" if group.description.blank? +- css_class += ' no-description' if group.description.blank? %li.group-row{ class: css_class } .controls.hidden-xs - = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-grouped btn-sm" - = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-grouped btn-sm btn-remove" + = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn btn-grouped btn-sm' + = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: 'btn btn-grouped btn-sm btn-remove' .stats %span @@ -18,7 +18,7 @@ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)} = visibility_level_icon(group.visibility_level, fw: false) - = image_tag group_icon(group), class: "avatar s40 hidden-xs" + = image_tag group_icon(group), class: 'avatar s40 hidden-xs' .title = link_to [:admin, group], class: 'group-name' do = group.name From 4b3747b2cf01e7878407649edacfa7c825537ca9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 4 Apr 2016 23:56:15 +0200 Subject: [PATCH 398/618] Add missing changelog item about improving navigation sidebar Signed-off-by: Dmitriy Zaporozhets --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index f72bb670ec..68ea96aaaa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -18,6 +18,7 @@ v 8.7.0 (unreleased) - Fix creation of merge requests for orphaned branches (Stan Hu) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) + - Improved UX of the navigation sidebar v 8.6.4 - Don't attempt to fetch any tags from a forked repo (Stan Hu) From f2005fa56682a5dc3b57d610cb733a34b2e08520 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Mon, 4 Apr 2016 19:35:39 -0300 Subject: [PATCH 399/618] Flush repository cache before import project data GitHub Pull Requests importer handle with the repository while importing data, we need to make sure that the cached values are valid. --- app/models/repository.rb | 5 +++++ app/services/projects/import_service.rb | 2 ++ spec/models/repository_spec.rb | 14 ++++++++++++++ spec/services/projects/import_service_spec.rb | 17 +++++++++++++++++ 4 files changed, 38 insertions(+) diff --git a/app/models/repository.rb b/app/models/repository.rb index e80c223840..a8e826c9cb 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -364,6 +364,11 @@ class Repository expire_tag_count_cache end + def before_import + expire_emptiness_caches + expire_exists_cache + end + # Runs code after a repository has been forked/imported. def after_import expire_emptiness_caches diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 2015897dd1..ef15ef6a47 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -46,6 +46,8 @@ module Projects def import_data return unless has_importer? + project.repository.before_import + unless importer.execute raise Error, 'The remote data could not be imported.' end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c5d5a1c249..f517f325c0 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -612,6 +612,20 @@ describe Repository, models: true do end end + describe '#before_import' do + it 'flushes the emptiness cachess' do + expect(repository).to receive(:expire_emptiness_caches) + + repository.before_import + end + + it 'flushes the exists cache' do + expect(repository).to receive(:expire_exists_cache) + + repository.before_import + end + end + describe '#after_import' do it 'flushes the emptiness cachess' do expect(repository).to receive(:expire_emptiness_caches) diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 04f474c736..32bf3acf48 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -72,6 +72,23 @@ describe Projects::ImportService, services: true do expect(result[:status]).to eq :success end + it 'flushes various caches' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository). + with(project.path_with_namespace, project.import_url). + and_return(true) + + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute). + and_return(true) + + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). + and_call_original + + expect_any_instance_of(Repository).to receive(:expire_exists_cache). + and_call_original + + subject.execute + end + it 'fails if importer fails' do expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false) From 21837bfb203d2d3ec85df7cb1d94b3b1818d4ab5 Mon Sep 17 00:00:00 2001 From: connorshea Date: Sat, 2 Apr 2016 01:07:22 -0600 Subject: [PATCH 400/618] Add comments to the SCSS Lint config file [ci skip] Also add some previously missing linters. --- .scss-lint.yml | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/.scss-lint.yml b/.scss-lint.yml index 3ce0c4901b..835a4a88c4 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -7,21 +7,44 @@ exclude: - 'app/assets/stylesheets/pages/emojis.scss' linters: + # Reports when you use improper spacing around ! (the "bang") in !default, + # !global, !important, and !optional flags. BangFormat: enabled: false + # Whether or not to prefer `border: 0` over `border: none`. BorderZero: enabled: false + # Reports when you define a rule set using a selector with chained classes + # (a.k.a. adjoining classes). + ChainedClasses: + enabled: false + + # Prefer hexadecimal color codes over color keywords. + # (e.g. `color: green` is a color keyword) ColorKeyword: enabled: false + # Prefer color literals (keywords or hexadecimal codes) to be used only in + # variable declarations. They should be referred to via variables everywhere + # else. ColorVariable: enabled: false + # Which form of comments to prefer in CSS. Comment: enabled: false + + # Reports @debug statements (which you probably left behind accidentally). + DebugStatement: + enabled: false + # Rule sets should be ordered as follows: + # - @extend declarations + # - @include declarations without inner @content + # - properties, @include declarations with inner @content + # - nested rule sets. DeclarationOrder: enabled: false @@ -32,15 +55,25 @@ linters: DisableLinterReason: enabled: true + # Reports when you define the same property twice in a single rule set. DuplicateProperty: enabled: false + # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: enabled: false + # Reports when you have an empty rule set. EmptyRule: enabled: false + # Reports when you have an @extend directive. + ExtendDirective: + enabled: false + + # Files should always have a final newline. This results in better diffs + # when adding lines to the file, since SCM systems such as git won't + # think that you touched the last line. FinalNewline: enabled: false @@ -53,12 +86,17 @@ linters: HexNotation: enabled: true + # Avoid using ID selectors. IdSelector: enabled: false + # The basenames of @imported SCSS partials should not begin with an + # underscore and should not include the filename extension. ImportPath: enabled: false + # Avoid using !important in properties. It is usually indicative of a + # misunderstanding of CSS specificity and can lead to brittle code. ImportantRule: enabled: false @@ -67,33 +105,51 @@ linters: enabled: true width: 2 + # Don't write leading zeros for numeric values with a decimal point. LeadingZero: enabled: false + # Reports when you define the same selector twice in a single sheet. MergeableSelector: enabled: false + # Functions, mixins, variables, and placeholders should be declared + # with all lowercase letters and hyphens instead of underscores. NameFormat: enabled: false + # Avoid nesting selectors too deeply. NestingDepth: enabled: false + # Always use placeholder selectors in @extend. PlaceholderInExtend: enabled: false + # Sort properties in a strict order. PropertySortOrder: enabled: false + # Reports when you use an unknown or disabled CSS property + # (ignoring vendor-prefixed properties). PropertySpelling: enabled: false + # Configure which units are allowed for property values. + PropertyUnits: + enabled: false + + # Pseudo-elements, like ::before, and ::first-letter, should be declared + # with two colons. Pseudo-classes, like :hover and :first-child, should + # be declared with one colon. PseudoElement: enabled: false + # Avoid qualifying elements in selectors (also known as "tag-qualifying"). QualifyingElement: enabled: false + # Don't write selectors with a depth of applicability greater than 3. SelectorDepth: enabled: false @@ -113,9 +169,12 @@ linters: enabled: true allow_single_line_rule_sets: true + # Split selectors onto separate lines after each comma, and have each + # individual selector occupy a single line. SingleLinePerSelector: enabled: false + # Commas in lists should be followed by a space. SpaceAfterComma: enabled: false @@ -128,29 +187,75 @@ linters: # colon. SpaceAfterPropertyName: enabled: true + + # Variables should be formatted with a single space separating the colon + # from the variable's value. + SpaceAfterVariableColon: + enabled: false + + # Variables should be formatted with no space between the name and the + # colon. + SpaceAfterVariableName: + enabled: false + # Operators should be formatted with a single space on both sides of an + # infix operator. SpaceAroundOperator: enabled: false # Opening braces should be preceded by a single space. SpaceBeforeBrace: enabled: true + + # Parentheses should not be padded with spaces. + SpaceBetweenParens: + enabled: false + # Enforces that string literals should be written with a consistent form + # of quotes (single or double). StringQuotes: enabled: false + # Property values, @extend, @include, and @import directives, and variable + # declarations should always end with a semicolon. TrailingSemicolon: enabled: false + # Reports lines containing trailing whitespace. TrailingWhitespace: enabled: false + # Don't write trailing zeros for numeric values with a decimal point. + TrailingZero: + enabled: false + + # Don't use the `all` keyword to specify transition properties. + TransitionAll: + enabled: false + + # Numeric values should not contain unnecessary fractional portions. UnnecessaryMantissa: enabled: false + # Do not use parent selector references (&) when they would otherwise + # be unnecessary. UnnecessaryParentReference: enabled: false + + # URLs should be valid and not contain protocols or domain names. + UrlFormat: + enabled: false + # URLs should always be enclosed within quotes. + UrlQuotes: + enabled: false + + # Properties, like color and font, are easier to read and maintain + # when defined using variables rather than literals. + VariableForProperty: + enabled: false + + # Avoid vendor prefixes. Or rather: don't write them yourself. VendorPrefix: enabled: false From 1a168279fa3eb87c2061917707397af21e7b26ea Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 4 Apr 2016 19:09:12 -0500 Subject: [PATCH 401/618] Prepare SAML for group retrieval --- lib/gitlab/saml/auth_hash.rb | 17 ++++++++++++++ lib/gitlab/saml/config.rb | 22 ++++++++++++++++++ lib/gitlab/saml/user.rb | 43 ++++++++++++++++++++++++++++++++++-- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 lib/gitlab/saml/auth_hash.rb create mode 100644 lib/gitlab/saml/config.rb diff --git a/lib/gitlab/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb new file mode 100644 index 0000000000..5ffccc0e10 --- /dev/null +++ b/lib/gitlab/saml/auth_hash.rb @@ -0,0 +1,17 @@ +module Gitlab + module Saml + class AuthHash < Gitlab::OAuth::AuthHash + + def groups + get_raw(Gitlab::Saml::Config.groups) + end + + private + + def get_raw(key) + auth_hash.extra[:raw_info][key] + end + + end + end +end diff --git a/lib/gitlab/saml/config.rb b/lib/gitlab/saml/config.rb new file mode 100644 index 0000000000..dade4c0fa6 --- /dev/null +++ b/lib/gitlab/saml/config.rb @@ -0,0 +1,22 @@ +# Load a specific server configuration +module Gitlab + module Saml + class Config + + class << self + def options + Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' } + end + + def groups + options['groups_attribute'] + end + + def external_groups + options['external_groups'] + end + end + + end + end +end diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb index b1e30110ef..14eda337d9 100644 --- a/lib/gitlab/saml/user.rb +++ b/lib/gitlab/saml/user.rb @@ -7,6 +7,11 @@ module Gitlab module Saml class User < Gitlab::OAuth::User + def initialize(auth_hash) + super + update_user_attributes + end + def save super('SAML') end @@ -18,7 +23,7 @@ module Gitlab @user ||= find_or_create_ldap_user end - if auto_link_saml_enabled? + if auto_link_saml_user? @user ||= find_by_email end @@ -37,11 +42,45 @@ module Gitlab end end + def changed? + gl_user.changed? || gl_user.identities.any?(&:changed?) + end + protected - def auto_link_saml_enabled? + def build_new_user + user = super + if external_users_enabled? + unless (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty? + user.external = true + end + end + user + end + + def auto_link_saml_user? Gitlab.config.omniauth.auto_link_saml_user end + + def external_users_enabled? + !Gitlab::Saml::Config.external_groups.nil? + end + + def auth_hash=(auth_hash) + @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash) + end + + def update_user_attributes + if persisted? + if external_users_enabled? + if (auth_hash.groups & Gitlab::Saml::Config.external_groups).empty? + gl_user.external = false + else + gl_user.external = true + end + end + end + end end end end From d14f080c0a7e4cfb2893a9fb2462a650f02a6ef9 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 4 Apr 2016 19:09:46 -0500 Subject: [PATCH 402/618] Remove unnecessary LDAP tests from SAML tests --- spec/lib/gitlab/saml/user_spec.rb | 66 ++++--------------------------- 1 file changed, 7 insertions(+), 59 deletions(-) diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb index de7cd99d49..6f5cf3a1cf 100644 --- a/spec/lib/gitlab/saml/user_spec.rb +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -23,6 +23,12 @@ describe Gitlab::Saml::User, lib: true do allow(Gitlab::LDAP::Config).to receive_messages(messages) end + def stub_saml_config(messages) + allow(Gitlab::Saml::Config).to receive_messages(messages) + end + + before { stub_saml_config({ options: { name: 'saml', args: {} } }) } + describe 'account exists on server' do before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } context 'and should bind with SAML' do @@ -138,7 +144,7 @@ describe Gitlab::Saml::User, lib: true do end describe 'blocking' do - before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) } + before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } context 'signup with SAML only' do context 'dont block on create' do @@ -162,64 +168,6 @@ describe Gitlab::Saml::User, lib: true do end end - context 'signup with linked omniauth and LDAP account' do - before do - stub_omniauth_config(auto_link_ldap_user: true) - allow(ldap_user).to receive(:uid) { uid } - allow(ldap_user).to receive(:username) { uid } - allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } - allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } - allow(saml_user).to receive(:ldap_person).and_return(ldap_user) - end - - context "and no account for the LDAP user" do - context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - - context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).to be_blocked - end - end - end - - context 'and LDAP user has an account already' do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } - - context 'dont block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - - context 'block on create (LDAP)' do - before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } - - it do - saml_user.save - expect(gl_user).to be_valid - expect(gl_user).not_to be_blocked - end - end - end - end - - context 'sign-in' do before do saml_user.save From 943d8d4b90fbdc4f68d548c0566343903f895138 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 4 Apr 2016 19:10:17 -0500 Subject: [PATCH 403/618] Config examples --- config/gitlab.yml.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index fb1c3476f6..c607e32a05 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -345,6 +345,8 @@ production: &base # # - { name: 'saml', # label: 'Our SAML Provider', + # groups_attribute: 'Groups', + # external_groups: ['Contractors', 'Freelancers'], # args: { # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', From e99855bfe4b4741d33d5575fdf1f0bc2edd85844 Mon Sep 17 00:00:00 2001 From: Patricio Cano Date: Mon, 4 Apr 2016 19:10:59 -0500 Subject: [PATCH 404/618] Avoid saving again if the user attributes haven't changed --- app/controllers/omniauth_callbacks_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 21135f7d60..d28e96c3f1 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -55,7 +55,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end else saml_user = Gitlab::Saml::User.new(oauth) - saml_user.save + saml_user.save if saml_user.changed? @user = saml_user.gl_user continue_login_process From 7a2370f74060b2f065e3602700fe1b33fda4685c Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 4 Apr 2016 21:25:38 -0400 Subject: [PATCH 405/618] Standardize the way we check for and display form errors - Some views had a "Close" button. We've removed this, because we don't want users accidentally hiding the validation errors and not knowing what needs to be fixed. - Some views used `li`, some used `p`, some used `span`. We've standardized on `li`. - Some views only showed the first error. We've standardized on showing all of them. - Some views added an `#error_explanation` div, which we've made standard. --- app/helpers/form_helper.rb | 18 ++++++++ app/views/abuse_reports/new.html.haml | 6 +-- app/views/admin/appearances/_form.html.haml | 5 +- .../application_settings/_form.html.haml | 6 +-- app/views/admin/applications/_form.html.haml | 7 +-- .../admin/broadcast_messages/_form.html.haml | 6 +-- app/views/admin/deploy_keys/new.html.haml | 6 +-- app/views/admin/groups/_form.html.haml | 5 +- app/views/admin/hooks/index.html.haml | 6 +-- app/views/admin/identities/_form.html.haml | 6 +-- app/views/admin/labels/_form.html.haml | 8 +--- app/views/admin/users/_form.html.haml | 6 +-- .../doorkeeper/applications/_form.html.haml | 6 +-- app/views/groups/edit.html.haml | 4 +- app/views/groups/new.html.haml | 5 +- app/views/profiles/keys/_form.html.haml | 6 +-- .../profiles/notifications/show.html.haml | 6 +-- app/views/profiles/passwords/edit.html.haml | 7 +-- app/views/profiles/passwords/new.html.haml | 7 +-- app/views/profiles/show.html.haml | 7 +-- app/views/projects/_errors.html.haml | 5 +- .../projects/deploy_keys/_form.html.haml | 6 +-- app/views/projects/hooks/index.html.haml | 6 +-- app/views/projects/labels/_form.html.haml | 8 +--- .../merge_requests/_new_compare.html.haml | 5 +- app/views/projects/milestones/_form.html.haml | 7 +-- .../protected_branches/index.html.haml | 6 +-- app/views/projects/variables/show.html.haml | 8 +--- app/views/projects/wikis/_form.html.haml | 6 +-- app/views/shared/_service_settings.html.haml | 7 +-- app/views/shared/issuable/_form.html.haml | 9 +--- app/views/shared/snippets/_form.html.haml | 6 +-- spec/helpers/form_helper_spec.rb | 46 +++++++++++++++++++ 33 files changed, 105 insertions(+), 153 deletions(-) create mode 100644 app/helpers/form_helper.rb create mode 100644 spec/helpers/form_helper_spec.rb diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb new file mode 100644 index 0000000000..6a43be2cf3 --- /dev/null +++ b/app/helpers/form_helper.rb @@ -0,0 +1,18 @@ +module FormHelper + def form_errors(model) + return unless model.errors.any? + + pluralized = 'error'.pluralize(model.errors.count) + headline = "The form contains the following #{pluralized}:" + + content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do + content_tag(:h4, headline) << + content_tag(:ul) do + model.errors.full_messages. + map { |msg| content_tag(:li, msg) }. + join. + html_safe + end + end + end +end diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index 3bc1b24b5e..06be1a5331 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -3,11 +3,9 @@ %p Please use this form to report users who create spam issues, comments or behave inappropriately. %hr = form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f| + = form_errors(@abuse_report) + = f.hidden_field :user_id - - if @abuse_report.errors.any? - .alert.alert-danger - - @abuse_report.errors.full_messages.each do |msg| - %p= msg .form-group = f.label :user_id, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 6f325914d1..d88f3ad314 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -1,8 +1,5 @@ = form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f| - - if @appearance.errors.any? - .alert.alert-danger - - @appearance.errors.full_messages.each do |msg| - %p= msg + = form_errors(@appearance) %fieldset.sign-in %legend diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index de86dacbb1..a8cca1a81c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -1,9 +1,5 @@ = form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| - - if @application_setting.errors.any? - #error_explanation - .alert.alert-danger - - @application_setting.errors.full_messages.each do |msg| - %p= msg + = form_errors(@application_setting) %fieldset %legend Visibility and Access Controls diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index e18f7b499d..4aacbb8cd7 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -1,9 +1,6 @@ = form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f| - - if application.errors.any? - .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × - - application.errors.full_messages.each do |msg| - %p= msg + = form_errors(application) + = content_tag :div, class: 'form-group' do = f.label :name, class: 'col-sm-2 control-label' .col-sm-10 diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index b748460a9f..6b157abf84 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -4,10 +4,8 @@ = render_broadcast_message(@broadcast_message.message.presence || "Your message here") = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| - -if @broadcast_message.errors.any? - .alert.alert-danger - - @broadcast_message.errors.full_messages.each do |msg| - %p= msg + = form_errors(@broadcast_message) + .form-group = f.label :message, class: 'control-label' .col-sm-10 diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index 5b46b3222a..15aa059c93 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -4,11 +4,7 @@ %div = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| - -if @deploy_key.errors.any? - .alert.alert-danger - %ul - - @deploy_key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@deploy_key) .form-group = f.label :title, class: "control-label" diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 7f2b1cd235..0cc405401c 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -1,8 +1,5 @@ = form_for [:admin, @group], html: { class: "form-horizontal" } do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first - + = form_errors(@group) = render 'shared/group_form', f: f .form-group.group-description-holder diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 53b3cd04c6..ad952052f2 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -10,10 +10,8 @@ = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| - -if @hook.errors.any? - .alert.alert-danger - - @hook.errors.full_messages.each do |msg| - %p= msg + = form_errors(@hook) + .form-group = f.label :url, "URL:", class: 'control-label' .col-sm-10 diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml index 3a78855822..112a201faf 100644 --- a/app/views/admin/identities/_form.html.haml +++ b/app/views/admin/identities/_form.html.haml @@ -1,9 +1,5 @@ = form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f| - - if @identity.errors.any? - #error_explanation - .alert.alert-danger - - @identity.errors.full_messages.each do |msg| - %p= msg + = form_errors(@identity) .form-group = f.label :provider, class: 'control-label' diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index 8c6b389bf1..448aa95354 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -1,11 +1,5 @@ = form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f| - -if @label.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - @label.errors.full_messages.each do |msg| - %span= msg - %br + = form_errors(@label) .form-group = f.label :title, class: 'control-label' diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index d2527ede99..b05fdbd555 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -1,10 +1,6 @@ .user_new = form_for [:admin, @user], html: { class: 'form-horizontal fieldset-form' } do |f| - -if @user.errors.any? - #error_explanation - .alert.alert-danger - - @user.errors.full_messages.each do |msg| - %p= msg + = form_errors(@user) %fieldset %legend Account diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 906b067615..5c98265727 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -1,9 +1,5 @@ = form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f| - - if application.errors.any? - .alert.alert-danger - %ul - - application.errors.full_messages.each do |msg| - %li= msg + = form_errors(application) .form-group = f.label :name, class: 'label-light' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index ea5a035839..a698cbbe9d 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -5,9 +5,7 @@ Group settings .panel-body = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first + = form_errors(@group) = render 'shared/group_form', f: f .form-group diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 30ab8aeba1..2b8bc269e6 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -6,10 +6,7 @@ %hr = form_for @group, html: { class: 'group-form form-horizontal' } do |f| - - if @group.errors.any? - .alert.alert-danger - %span= @group.errors.full_messages.first - + = form_errors(@group) = render 'shared/group_form', f: f, autofocus: true .form-group.group-description-holder diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index 4d78215ed3..b3ed59a1a4 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -1,10 +1,6 @@ %div = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f| - - if @key.errors.any? - .alert.alert-danger - %ul - - @key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@key) .form-group = f.label :key, class: 'label-light' diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 3d15c0d932..6609295a2a 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -2,11 +2,7 @@ - header_title page_title, profile_notifications_path = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| - -if @user.errors.any? - %div.alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + = form_errors(@user) = hidden_field_tag :notification_type, 'global' .row diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 44d758dceb..5ac8a8b9d0 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -13,11 +13,8 @@ - unless @user.password_automatically_set? or recover your current one = form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f| - -if @user.errors.any? - .alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + = form_errors(@user) + - unless @user.password_automatically_set? .form-group = f.label :current_password, class: 'label-light' diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index d165f758c8..2eb9fac57c 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -7,11 +7,8 @@ Please set a new password before proceeding. %br After a successful password update you will be redirected to login screen. - -if @user.errors.any? - .alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + + = form_errors(@user) - unless @user.password_automatically_set? .form-group diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index dcb3be9585..f59d27f7ed 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,9 +1,6 @@ = form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f| - -if @user.errors.any? - %div.alert.alert-danger - %ul - - @user.errors.full_messages.each do |msg| - %li= msg + = form_errors(@user) + .row .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 diff --git a/app/views/projects/_errors.html.haml b/app/views/projects/_errors.html.haml index 7c8bb33ed7..2dba22d3be 100644 --- a/app/views/projects/_errors.html.haml +++ b/app/views/projects/_errors.html.haml @@ -1,4 +1 @@ -- if @project.errors.any? - .alert.alert-danger - %button{ type: "button", class: "close", "data-dismiss" => "alert"} × - = @project.errors.full_messages.first += form_errors(@project) diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index 5e182af266..f6565f8583 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,10 +1,6 @@ %div = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f| - -if @key.errors.any? - .alert.alert-danger - %ul - - @key.errors.full_messages.each do |msg| - %li= msg + = form_errors(@key) .form-group = f.label :title, class: "control-label" diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 67d016bd87..e39224d86c 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -9,10 +9,8 @@ %hr.clearfix = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - -if @hook.errors.any? - .alert.alert-danger - - @hook.errors.full_messages.each do |msg| - %p= msg + = form_errors(@hook) + .form-group = f.label :url, "URL", class: 'control-label' .col-sm-10 diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml index be7a0bb562..aa143e54ff 100644 --- a/app/views/projects/labels/_form.html.haml +++ b/app/views/projects/labels/_form.html.haml @@ -1,11 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| - -if @label.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - @label.errors.full_messages.each do |msg| - %span= msg - %br + = form_errors(@label) .form-group = f.label :title, class: 'control-label' diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 01dc7519be..0931f743a3 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -28,10 +28,7 @@ .mr_target_commit - if @merge_request.errors.any? - .alert.alert-danger - - @merge_request.errors.full_messages.each do |msg| - %div= msg - + = form_errors(@merge_request) - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present? .light-well.append-bottom-default .center diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 23f2bca7ba..b2dae1c70e 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,9 +1,6 @@ = form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f| - -if @milestone.errors.any? - .alert.alert-danger - %ul - - @milestone.errors.full_messages.each do |msg| - %li= msg + = form_errors(@milestone) + .row .col-md-6 .form-group diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index cfd7e1534c..653b02da4d 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -13,11 +13,7 @@ - if can? current_user, :admin_project, @project = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'form-horizontal' } do |f| - -if @protected_branch.errors.any? - .alert.alert-danger - %ul - - @protected_branch.errors.full_messages.each do |msg| - %li= msg + = form_errors(@protected_branch) .form-group = f.label :name, "Branch", class: 'control-label' diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml index efe1e6f24c..ca284b84d3 100644 --- a/app/views/projects/variables/show.html.haml +++ b/app/views/projects/variables/show.html.haml @@ -13,13 +13,7 @@ = nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| - - if @project.errors.any? - #error_explanation - %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:" - .alert.alert-error - %ul - - @project.errors.full_messages.each do |msg| - %li= msg + = form_errors(@project) = f.fields_for :variables do |variable_form| .form-group diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index f0d1932e23..812876e283 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,9 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f| - -if @page.errors.any? - #error_explanation - .alert.alert-danger - - @page.errors.full_messages.each do |msg| - %p= msg + = form_errors(@page) = f.hidden_field :title, value: @page.title .form-group diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 5a60ff5a5d..fc935166bf 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -1,9 +1,4 @@ -- if @service.errors.any? - #error_explanation - .alert.alert-danger - %ul - - @service.errors.full_messages.each do |msg| - %li= msg += form_errors(@service) - if @service.help.present? .well diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index e2a9e5bfb9..0cda785f91 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,10 +1,5 @@ -- if issuable.errors.any? - .row - .col-sm-offset-2.col-sm-10 - .alert.alert-danger - - issuable.errors.full_messages.each do |msg| - %span= msg - %br += form_errors(issuable) + .form-group = f.label :title, class: 'control-label' .col-sm-10 diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 1041eccd1d..47ec09f62c 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,10 +1,6 @@ .snippet-form-holder = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| - - if @snippet.errors.any? - .alert.alert-danger - %ul - - @snippet.errors.full_messages.each do |msg| - %li= msg + = form_errors(@snippet) .form-group = f.label :title, class: 'control-label' diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb new file mode 100644 index 0000000000..b20373a96f --- /dev/null +++ b/spec/helpers/form_helper_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +describe FormHelper do + describe 'form_errors' do + it 'returns nil when model has no errors' do + model = double(errors: []) + + expect(helper.form_errors(model)).to be_nil + end + + it 'renders an alert div' do + model = double(errors: errors_stub('Error 1')) + + expect(helper.form_errors(model)). + to include('
    ') + end + + it 'contains a summary message' do + single_error = double(errors: errors_stub('A')) + multi_errors = double(errors: errors_stub('A', 'B', 'C')) + + expect(helper.form_errors(single_error)). + to include('

    The form contains the following error:') + expect(helper.form_errors(multi_errors)). + to include('

    The form contains the following errors:') + end + + it 'renders each message' do + model = double(errors: errors_stub('Error 1', 'Error 2', 'Error 3')) + + errors = helper.form_errors(model) + + aggregate_failures do + expect(errors).to include('
  • Error 1
  • ') + expect(errors).to include('
  • Error 2
  • ') + expect(errors).to include('
  • Error 3
  • ') + end + end + + def errors_stub(*messages) + ActiveModel::Errors.new(double).tap do |errors| + messages.each { |msg| errors.add(:base, msg) } + end + end + end +end From b9abf938edf52e762d320b1eb8732155e23d7b72 Mon Sep 17 00:00:00 2001 From: connorshea Date: Wed, 30 Mar 2016 16:03:49 -0600 Subject: [PATCH 406/618] Wrap images in discussions and wikis with a link to the image source using ImageLinkFilter. Resolves #14411. See merge request !3464 --- CHANGELOG | 1 + .../stylesheets/framework/typography.scss | 6 +++++ features/steps/project/wiki.rb | 2 +- lib/banzai/filter/image_link_filter.rb | 27 +++++++++++++++++++ lib/banzai/pipeline/gfm_pipeline.rb | 1 + spec/features/atom/users_spec.rb | 2 +- .../banzai/filter/image_link_filter_spec.rb | 24 +++++++++++++++++ 7 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 lib/banzai/filter/image_link_filter.rb create mode 100644 spec/lib/banzai/filter/image_link_filter_spec.rb diff --git a/CHANGELOG b/CHANGELOG index f72bb670ec..39239bebcf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) + - All images in discussions and wikis now link to their source files !3464 (Connor Shea). - Improved Markdown rendering performance !3389 (Yorick Peterse) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) - Preserve time notes/comments have been updated at when moving issue diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index b1886fbe67..c3c7bc9fdb 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -138,6 +138,12 @@ } } + a.no-attachment-icon { + &:before { + display: none; + } + } + /* Link to current header. */ h1, h2, h3, h4, h5, h6 { position: relative; diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb index 223b7277b5..9f6aed1c5b 100644 --- a/features/steps/project/wiki.rb +++ b/features/steps/project/wiki.rb @@ -85,7 +85,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps end step 'I have an existing Wiki page with images linked on page' do - wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![image](image.jpg)", :markdown, "first commit") + wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![alt text](image.jpg)", :markdown, "first commit") @wiki_page = wiki.find_page("pictures") end diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb new file mode 100644 index 0000000000..ccd106860b --- /dev/null +++ b/lib/banzai/filter/image_link_filter.rb @@ -0,0 +1,27 @@ +module Banzai + module Filter + # HTML filter that wraps links around inline images. + class ImageLinkFilter < HTML::Pipeline::Filter + + # Find every image that isn't already wrapped in an `a` tag, create + # a new node (a link to the image source), copy the image as a child + # of the anchor, and then replace the img with the link-wrapped version. + def call + doc.xpath('descendant-or-self::img[not(ancestor::a)]').each do |img| + + link = doc.document.create_element( + 'a', + class: 'no-attachment-icon', + href: img['src'], + target: '_blank' + ) + + link.children = img.clone + img.replace(link) + end + + doc + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 8cd4b50e65..ed3cfd6b02 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -7,6 +7,7 @@ module Banzai Filter::SanitizationFilter, Filter::UploadLinkFilter, + Filter::ImageLinkFilter, Filter::EmojiFilter, Filter::TableOfContentsFilter, Filter::AutolinkFilter, diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index dc41be8246..de6aed74fb 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -61,7 +61,7 @@ describe "User Feed", feature: true do end it 'should have XHTML summaries in merge request descriptions' do - expect(body).to match /Here is the fix: ]*\/>/ + expect(body).to match /Here is the fix: ]*>]*\/><\/a>/ end end end diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb new file mode 100644 index 0000000000..dd5594750c --- /dev/null +++ b/spec/lib/banzai/filter/image_link_filter_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Banzai::Filter::ImageLinkFilter, lib: true do + include FilterSpecHelper + + def image(path) + %() + end + + it 'wraps the image with a link to the image src' do + doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')) + expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] + end + + it 'does not wrap a duplicate link' do + exp = act = %q(
    #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}) + expect(filter(act).to_html).to eq exp + end + + it 'works with external images' do + doc = filter(image('https://i.imgur.com/DfssX9C.jpg')) + expect(doc.at_css('img')['src']).to eq doc.at_css('a')['href'] + end +end From 8e3f5ebaa080bec65b35c21b8a70f84c7ed9fa63 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 09:07:40 +0100 Subject: [PATCH 407/618] CS multiline --- app/assets/javascripts/gl_dropdown.js.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 466213496e..6a825a67a1 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -331,11 +331,11 @@ class GitLabDropdown ).join('') noResults: -> - html = "" + html = "" highlightRow: (index) -> if @filterInput.val() isnt "" From 4d9d06ec5057c512bd05ab5883ca842c7a3a3c9a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 09:12:26 +0100 Subject: [PATCH 408/618] Removed emoji button from notes form --- app/views/projects/notes/_hints.html.haml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 0c6758210b..0b00204340 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -1,7 +1,4 @@ .comment-toolbar.clearfix - %button.toolbar-button.js-toolbar-button{ type: 'button', data: { prefix: ':' }, tabindex: '-1' } - = icon('smile-o', class: 'toolbar-button-icon') - Emoji .toolbar-text Styling with = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 From 831d1807df6d1d9f8213898614a24bea78208114 Mon Sep 17 00:00:00 2001 From: "P.S.V.R" Date: Tue, 5 Apr 2016 16:20:08 +0800 Subject: [PATCH 409/618] Eliminate ugly scroll bars shown in modal boxes --- app/assets/stylesheets/pages/help.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index bd224705f0..604f1700cf 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -59,6 +59,9 @@ position: relative; overflow-y: auto; padding: 15px; + .form-actions { + margin: -$gl-padding+1; + } } body.modal-open { From b8d1545bf18e672a68b9095e4c9c6cd6c018aad7 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 5 Apr 2016 10:54:34 +0200 Subject: [PATCH 410/618] Update language after doing all other operations --- app/services/git_push_service.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c007d648dd..c76c118df1 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -43,13 +43,14 @@ class GitPushService < BaseService @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) process_commit_messages end - # Checks if the main language has changed in the project and if so - # it updates it accordingly - update_main_language # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. update_merge_requests + # Checks if the main language has changed in the project and if so + # it updates it accordingly + update_main_language + perform_housekeeping end From aad3b6ddf88e31072602af7d1d06f64e1823673b Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Tue, 5 Apr 2016 11:08:41 +0200 Subject: [PATCH 411/618] Update language only on HEAD of the repository --- app/services/git_push_service.rb | 3 +++ spec/services/git_push_service_spec.rb | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index c76c118df1..36c9ee92da 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -55,6 +55,9 @@ class GitPushService < BaseService end def update_main_language + return unless is_default_branch? + return unless push_to_new_branch? || push_to_existing_branch? + current_language = @project.repository.main_language unless current_language == @project.main_language diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 8490a729e5..1047e32960 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -159,18 +159,28 @@ describe GitPushService, services: true do end describe "Updates main language" do - context "before push" do it { expect(project.main_language).to eq(nil) } end context "after push" do before do - @service = execute_service(project, user, @oldrev, @newrev, @ref) + @service = execute_service(project, user, @oldrev, @newrev, ref) end - it { expect(@service.update_main_language).to eq(true) } - it { expect(project.main_language).to eq("Ruby") } + context "to master" do + let(:ref) { @ref } + + it { expect(@service.update_main_language).to eq(true) } + it { expect(project.main_language).to eq("Ruby") } + end + + context "to other branch" do + let(:ref) { 'refs/heads/feature/branch' } + + it { expect(@service.update_main_language).to eq(nil) } + it { expect(project.main_language).to eq(nil) } + end end end From b248ee93814e8521fa0c73c82ec9ed113698b945 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Apr 2016 13:29:48 +0200 Subject: [PATCH 412/618] Check permissions when importing project members Closes #14899 --- CHANGELOG | 3 ++ .../projects/project_members_controller.rb | 9 +++- .../project_members_controller_spec.rb | 49 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 spec/controllers/projects/project_members_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 39239bebcf..362a571bb4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,9 @@ v 8.7.0 (unreleased) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) +v 8.6.5 (unreleased) + - Check permissions when user attempts to import members from another project + v 8.6.4 - Don't attempt to fetch any tags from a forked repo (Stan Hu) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e7bddc4a6f..cd984f03c6 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -95,8 +95,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController def apply_import giver = Project.find(params[:source_project_id]) - status = @project.team.import(giver, current_user) - notice = status ? "Successfully imported" : "Import failed" + + if current_user.can?(:read_project_member, giver) + status = @project.team.import(giver, current_user) + notice = status ? "Successfully imported" : "Import failed" + else + notice = 'You are not authorized to import members from this project' + end redirect_to(namespace_project_project_members_path(project.namespace, project), notice: notice) diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb new file mode 100644 index 0000000000..6d1df8d9fb --- /dev/null +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -0,0 +1,49 @@ +require('spec_helper') + +describe Projects::ProjectMembersController do + let(:project) { create(:project) } + let(:another_project) { create(:project, :private) } + let(:user) { create(:user) } + let(:member) { create(:user) } + + before do + project.team << [user, :master] + another_project.team << [member, :guest] + sign_in(user) + end + + describe '#apply_import' do + shared_context 'import applied' do + before do + post(:apply_import, namespace_id: project.namespace.to_param, + project_id: project.to_param, + source_project_id: another_project.id) + end + end + + context 'when user can access source project members' do + before { another_project.team << [user, :guest] } + include_context 'import applied' + + it 'imports source project members' do + expect(project.team_members).to include member + expect(response).to set_flash.to 'Successfully imported' + expect(response).to redirect_to( + namespace_project_project_members_path(project.namespace, project) + ) + end + end + + context 'when user is not member of a source project' do + include_context 'import applied' + + it 'does not import team members' do + expect(project.team_members).to_not include member + end + + it 'notifies about invalid permissions' do + expect(response).to set_flash.to /not authorized/ + end + end + end +end From c52b5c92fbd31dc6f76087c43a94243d382d3172 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Apr 2016 13:55:15 +0200 Subject: [PATCH 413/618] Do not leak project exists when importing members When importing members, and user does not have permissions to read members in a source project, do not leak information about source project existence. Notifiy user that project has not been found instead. --- app/controllers/projects/project_members_controller.rb | 8 ++++---- .../projects/project_members_controller_spec.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index cd984f03c6..fd56dfd126 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -94,13 +94,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def apply_import - giver = Project.find(params[:source_project_id]) + source_project = Project.find(params[:source_project_id]) - if current_user.can?(:read_project_member, giver) - status = @project.team.import(giver, current_user) + if can?(current_user, :read_project_member, source_project) + status = @project.team.import(source_project, current_user) notice = status ? "Successfully imported" : "Import failed" else - notice = 'You are not authorized to import members from this project' + notice = 'Import failed - source project not found!' end redirect_to(namespace_project_project_members_path(project.namespace, project), diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 6d1df8d9fb..6ff3d4199f 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -41,8 +41,8 @@ describe Projects::ProjectMembersController do expect(project.team_members).to_not include member end - it 'notifies about invalid permissions' do - expect(response).to set_flash.to /not authorized/ + it 'pretends that source projects does not exist' do + expect(response).to set_flash.to /source project not found/ end end end From f22e6515df8c062c3a1c29e21c4d846a9e000c9d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Apr 2016 13:36:00 +0100 Subject: [PATCH 414/618] Added CHANGELOG for build notifications [ci skip] --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index f0b5e58ded..e1b6b32cff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,7 @@ v 8.7.0 (unreleased) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) - Improved UX of the navigation sidebar + - Build status notifications v 8.6.4 - Don't attempt to fetch any tags from a forked repo (Stan Hu) From bb9c194c23b8b3ffef30c7fdbe244d4fefc93883 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 5 Apr 2016 14:37:06 +0200 Subject: [PATCH 415/618] Respond 404 when unauthorized user imports members --- app/controllers/projects/project_members_controller.rb | 2 +- spec/controllers/projects/project_members_controller_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index fd56dfd126..e457db2f0b 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -100,7 +100,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController status = @project.team.import(source_project, current_user) notice = status ? "Successfully imported" : "Import failed" else - notice = 'Import failed - source project not found!' + return render_404 end redirect_to(namespace_project_project_members_path(project.namespace, project), diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 6ff3d4199f..d47e4ab9a4 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -41,8 +41,8 @@ describe Projects::ProjectMembersController do expect(project.team_members).to_not include member end - it 'pretends that source projects does not exist' do - expect(response).to set_flash.to /source project not found/ + it 'responds with not found' do + expect(response.status).to eq 404 end end end From 7b290e29f0c7484617d4f1c09244db84c00c11b4 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 5 Apr 2016 17:04:16 +0300 Subject: [PATCH 416/618] Fix LDAP link and codeblock indentation [ci skip] --- doc/administration/auth/ldap.md | 12 ++++++------ doc/integration/ldap.md | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 237700bbcd..1009677984 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -261,13 +261,13 @@ tree and traverse it. - 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 + ```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 - ``` + # For installations from source + sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production + ``` ### Connection Refused diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index fb20308c49..30f0c15dac 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -1,3 +1,3 @@ # GitLab LDAP integration -This document was moved under [`administration/auth/ldap`](administration/auth/ldap.md). +This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md). From 1ba9a91c6d3b98e1825e173fe281ba065d35890c Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Thu, 31 Mar 2016 16:11:49 -0300 Subject: [PATCH 417/618] Fix problem when creating milestones in groups without projects --- .../groups/milestones_controller.rb | 28 +++++++++++++++---- .../groups/milestones_controller_spec.rb | 6 ++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index b23c3022fb..2c05d9e0fe 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -18,14 +18,14 @@ class Groups::MilestonesController < Groups::ApplicationController end def create - project_ids = params[:milestone][:project_ids] + project_ids = params[:milestone][:project_ids].reject(&:blank?) title = milestone_params[:title] - @projects.where(id: project_ids).each do |project| - Milestones::CreateService.new(project, current_user, milestone_params).execute + if project_ids.present? + create_milestones(project_ids, title) + else + render_new_with_error("Select a project(s).") end - - redirect_to milestone_path(title) end def show @@ -41,6 +41,24 @@ class Groups::MilestonesController < Groups::ApplicationController private + def create_milestones(project_ids, title) + begin + @projects.where(id: project_ids).each do |project| + ActiveRecord::Base.transaction { Milestones::CreateService.new(project, current_user, milestone_params).execute } + end + + redirect_to milestone_path(title) + rescue => e + render_new_with_error("Error creating milestones: #{e.message}") + end + end + + def render_new_with_error(error) + @milestone = Milestone.new(milestone_params) + flash[:alert] = error + render :new + end + def authorize_admin_milestones! return render_404 unless can?(current_user, :admin_milestones, group) end diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index eb0c6ac6d8..f258d6fa0c 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -23,5 +23,11 @@ describe Groups::MilestonesController do expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title)) expect(Milestone.where(title: title).count).to eq(2) end + + it "redirects to new when there are no project ids" do + post :create, group_id: group.id, milestone: { title: title, project_ids: [""] } + expect(response).to render_template :new + expect(flash[:alert]).to_not be_nil + end end end From 5d428030451b1fa2bac89f798c40d2f91ac65bac Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 1 Apr 2016 15:50:17 -0300 Subject: [PATCH 418/618] Improve code --- .../groups/milestones_controller.rb | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 2c05d9e0fe..21fc329f23 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -21,10 +21,10 @@ class Groups::MilestonesController < Groups::ApplicationController project_ids = params[:milestone][:project_ids].reject(&:blank?) title = milestone_params[:title] - if project_ids.present? - create_milestones(project_ids, title) + if create_milestones(project_ids, title) + redirect_to milestone_path(title) else - render_new_with_error("Select a project(s).") + render_new_with_error(@error) end end @@ -42,14 +42,22 @@ class Groups::MilestonesController < Groups::ApplicationController private def create_milestones(project_ids, title) + unless project_ids.present? + @error = "Please select at least one project." + return false + end + begin - @projects.where(id: project_ids).each do |project| - ActiveRecord::Base.transaction { Milestones::CreateService.new(project, current_user, milestone_params).execute } + ActiveRecord::Base.transaction do + @projects.where(id: project_ids).each do |project| + Milestones::CreateService.new(project, current_user, milestone_params).execute + end end - redirect_to milestone_path(title) + true rescue => e - render_new_with_error("Error creating milestones: #{e.message}") + @error = "Error creating milestone: #{e.message}." + false end end From 32c7e42b612bdda43eeef55d8c8afdc9eeb33785 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Mon, 4 Apr 2016 17:04:35 -0300 Subject: [PATCH 419/618] Improve code --- .../groups/milestones_controller.rb | 31 +++++++++---------- app/views/groups/milestones/new.html.haml | 8 +++++ .../groups/milestones_controller_spec.rb | 2 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 21fc329f23..fcf19e8066 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -24,7 +24,7 @@ class Groups::MilestonesController < Groups::ApplicationController if create_milestones(project_ids, title) redirect_to milestone_path(title) else - render_new_with_error(@error) + render_new_with_error(project_ids.empty?) end end @@ -42,28 +42,25 @@ class Groups::MilestonesController < Groups::ApplicationController private def create_milestones(project_ids, title) - unless project_ids.present? - @error = "Please select at least one project." - return false - end + return false unless project_ids.present? - begin - ActiveRecord::Base.transaction do - @projects.where(id: project_ids).each do |project| - Milestones::CreateService.new(project, current_user, milestone_params).execute - end + ActiveRecord::Base.transaction do + @projects.where(id: project_ids).each do |project| + Milestones::CreateService.new(project, current_user, milestone_params).execute end - - true - rescue => e - @error = "Error creating milestone: #{e.message}." - false end + + true + + rescue => e + + flash.now[:alert] = "An error occurred while creating the milestone: #{e.message}" + false end - def render_new_with_error(error) + def render_new_with_error(empty_project_ids) @milestone = Milestone.new(milestone_params) - flash[:alert] = error + @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids render :new end diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index a8e1ed77da..4290e0bf72 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -10,6 +10,14 @@ = form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input' } do |f| .row + - if @milestone.errors.any? + #error_explanation + .alert.alert-danger + %ul + - @milestone.errors.full_messages.each do |msg| + %li + = msg + .col-md-6 .form-group = f.label :title, "Title", class: "control-label" diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index f258d6fa0c..9c7b5c74b8 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -27,7 +27,7 @@ describe Groups::MilestonesController do it "redirects to new when there are no project ids" do post :create, group_id: group.id, milestone: { title: title, project_ids: [""] } expect(response).to render_template :new - expect(flash[:alert]).to_not be_nil + expect(assigns(:milestone).errors).to_not be_nil end end end From c8d0f355022956e557c148fd5b5a3176661abba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 5 Apr 2016 18:50:43 +0200 Subject: [PATCH 420/618] Add 8.5.9 CHANGELOG item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index e1b6b32cff..071d02f916 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -140,6 +140,9 @@ v 8.6.0 - Trigger a todo for mentions on commits page - Let project owners and admins soft delete issues and merge requests +v 8.5.9 + - Don't attempt to fetch any tags from a forked repo (Stan Hu). + v 8.5.8 - Bump Git version requirement to 2.7.4 From 9f33bf86cf55df8a00357f49bc63a5291b2a8024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Tue, 5 Apr 2016 18:56:09 +0200 Subject: [PATCH 421/618] Add 8.4.6 and 8.4.7 CHANGELOG items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 071d02f916..a4bb4589f3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -284,6 +284,12 @@ v 8.5.0 - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Add Todos +v 8.4.7 + - Don't attempt to fetch any tags from a forked repo (Stan Hu). + +v 8.4.6 + - Bump Git version requirement to 2.7.4 + v 8.4.5 - No CE-specific changes From 5ff9d2a1821ef0413a349388bcdc0fab84a17086 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 28 Mar 2016 15:37:53 -0400 Subject: [PATCH 422/618] Bump rails to 4.2.6 --- Gemfile | 2 +- Gemfile.lock | 72 ++++++++++++++++++------------------- config/environments/test.rb | 1 + 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/Gemfile b/Gemfile index 6327227282..5eaaf0cfb1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -gem 'rails', '4.2.5.2' +gem 'rails', '4.2.6' gem 'rails-deprecated_sanitizer', '~> 1.0.3' # Responders respond_to and respond_with diff --git a/Gemfile.lock b/Gemfile.lock index 229089f431..df18410ece 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,41 +4,41 @@ GEM CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (2.0.1) - actionmailer (4.2.5.2) - actionpack (= 4.2.5.2) - actionview (= 4.2.5.2) - activejob (= 4.2.5.2) + actionmailer (4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.5.2) - actionview (= 4.2.5.2) - activesupport (= 4.2.5.2) + actionpack (4.2.6) + actionview (= 4.2.6) + activesupport (= 4.2.6) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.5.2) - activesupport (= 4.2.5.2) + actionview (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.5.2) - activesupport (= 4.2.5.2) + activejob (4.2.6) + activesupport (= 4.2.6) globalid (>= 0.3.0) - activemodel (4.2.5.2) - activesupport (= 4.2.5.2) + activemodel (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) - activerecord (4.2.5.2) - activemodel (= 4.2.5.2) - activesupport (= 4.2.5.2) + activerecord (4.2.6) + activemodel (= 4.2.6) + activesupport (= 4.2.6) arel (~> 6.0) activerecord-deprecated_finders (1.0.4) activerecord-session_store (0.1.2) actionpack (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5) railties (>= 4.0.0, < 5) - activesupport (4.2.5.2) + activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) @@ -464,8 +464,8 @@ GEM nokogiri (>= 1.5.9) macaddr (1.7.1) systemu (~> 2.6.2) - mail (2.6.3) - mime-types (>= 1.16, < 3) + mail (2.6.4) + mime-types (>= 1.16, < 4) mail_room (0.6.1) method_source (0.8.2) mime-types (1.25.1) @@ -595,16 +595,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.5.2) - actionmailer (= 4.2.5.2) - actionpack (= 4.2.5.2) - actionview (= 4.2.5.2) - activejob (= 4.2.5.2) - activemodel (= 4.2.5.2) - activerecord (= 4.2.5.2) - activesupport (= 4.2.5.2) + rails (4.2.6) + actionmailer (= 4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) + activemodel (= 4.2.6) + activerecord (= 4.2.6) + activesupport (= 4.2.6) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.5.2) + railties (= 4.2.6) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -614,9 +614,9 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - railties (4.2.5.2) - actionpack (= 4.2.5.2) - activesupport (= 4.2.5.2) + railties (4.2.6) + actionpack (= 4.2.6) + activesupport (= 4.2.6) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) @@ -788,10 +788,10 @@ GEM spring (>= 0.9.1) sprockets (3.3.5) rack (> 1, < 3) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) + sprockets-rails (3.0.4) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) state_machines (0.4.0) state_machines-activemodel (0.3.0) activemodel (~> 4.1) @@ -1002,7 +1002,7 @@ DEPENDENCIES rack-attack (~> 4.3.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) - rails (= 4.2.5.2) + rails (= 4.2.6) rails-deprecated_sanitizer (~> 1.0.3) raphael-rails (~> 2.1.2) rblineprof diff --git a/config/environments/test.rb b/config/environments/test.rb index f96ac6f975..a703c0934f 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -8,6 +8,7 @@ Rails.application.configure do config.cache_classes = false # Configure static asset server for tests with Cache-Control for performance + config.assets.digest = false config.serve_static_files = true config.static_cache_control = "public, max-age=3600" From 400a7b995de5c35fa3a2df83e08867c736f72324 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Tue, 5 Apr 2016 13:24:58 -0400 Subject: [PATCH 423/618] Attribution where it is due. --- doc/markdown/markdown.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index e6eb1cf381..75d55b3417 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -31,7 +31,7 @@ _GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._ -For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. +For GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). You can use GFM in @@ -47,15 +47,15 @@ You can also use other rich text files in GitLab. You might have to install a de GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p). -A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. +A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. Line-breaks, or softreturns, are rendered if you end a line with two or more spaces - Roses are red [followed by two or more spaces] + Roses are red [followed by two or more spaces] Violets are blue Sugar is sweet -Roses are red +Roses are red Violets are blue Sugar is sweet @@ -67,7 +67,7 @@ It is not reasonable to italicize just _part_ of a word, especially when you're perform_complicated_task do_this_and_do_that_and_another_thing -perform_complicated_task +perform_complicated_task do_this_and_do_that_and_another_thing ## URL auto-linking @@ -534,7 +534,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. ``` @@ -546,7 +546,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also begins a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. From b8d0f174224c883d2b6766ba424a7b5ad7f2431e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 5 Apr 2016 14:06:56 -0400 Subject: [PATCH 424/618] The Markdown doc is one place where trailing whitespace matters [ci skip] --- doc/markdown/markdown.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md index 75d55b3417..4f199b6af6 100644 --- a/doc/markdown/markdown.md +++ b/doc/markdown/markdown.md @@ -31,7 +31,7 @@ _GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._ -For GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). +GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). You can use GFM in @@ -55,7 +55,7 @@ Line-breaks, or softreturns, are rendered if you end a line with two or more spa Sugar is sweet -Roses are red +Roses are red Violets are blue Sugar is sweet @@ -534,7 +534,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. ``` @@ -546,7 +546,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa This line is also begins a separate paragraph, but... This line is only separated by a single newline, so it's a separate line in the *same paragraph*. -This line is also a separate paragraph, and... +This line is also a separate paragraph, and... This line is on its own line, because the previous line ends with two spaces. From 934f1e9097485bbaebbe2759e995c77bb4391c5d Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 4 Apr 2016 14:59:54 -0700 Subject: [PATCH 425/618] Fix Error 500 after renaming a project path Closes #14885 --- CHANGELOG | 3 ++- app/controllers/projects_controller.rb | 3 +++ spec/controllers/projects_controller_spec.rb | 22 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e1b6b32cff..7d744a6c49 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,11 +3,12 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.7.0 (unreleased) - All images in discussions and wikis now link to their source files !3464 (Connor Shea). - Improved Markdown rendering performance !3389 (Yorick Peterse) - - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu) + - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) - Preserve time notes/comments have been updated at when moving issue - Make HTTP(s) label consistent on clone bar (Stan Hu) - Expose label description in API (Mariusz Jachimowicz) - Allow back dating on issues when created through the API + - Fix Error 500 after renaming a project path (Stan Hu) - Fix avatar stretching by providing a cropping feature - Add endpoints to archive or unarchive a project !3372 - Add links to CI setup documentation from project settings and builds pages diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8c3a74c823..3cc37e5985 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -40,6 +40,9 @@ class ProjectsController < Projects::ApplicationController def update status = ::Projects::UpdateService.new(@project, current_user, project_params).execute + # Refresh the repo in case anything changed + @repository = project.repository + respond_to do |format| if status flash[:notice] = "Project '#{@project.name}' was successfully updated." diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 1893e946f5..069cd917e5 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -83,6 +83,28 @@ describe ProjectsController do end end + describe "#update" do + render_views + + let(:admin) { create(:admin) } + + it "sets the repository to the right path after a rename" do + new_path = 'renamed_path' + project_params = { path: new_path } + controller.instance_variable_set(:@project, project) + sign_in(admin) + + put :update, + namespace_id: project.namespace.to_param, + id: project.id, + project: project_params + + expect(project.repository.path).to include(new_path) + expect(assigns(:repository).path).to eq(project.repository.path) + expect(response.status).to eq(200) + end + end + describe "#destroy" do let(:admin) { create(:admin) } From 33faf219aaf72bb2c8a6df06fd42adf17bebfd14 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 5 Apr 2016 11:43:30 -0700 Subject: [PATCH 426/618] Fix data check in update issue response --- app/assets/javascripts/issue.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index d663e34871..b30e493592 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -49,7 +49,7 @@ class @Issue issueStatus = if isClose then 'close' else 'open' new Flash(issueFailMessage, 'alert') success: (data, textStatus, jqXHR) -> - if data.saved + if 'id' of data $(document).trigger('issuable:change'); if isClose $('a.btn-close').addClass('hidden') From 0ab6b82a234f6dcc208591724036f152eca0e1c4 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 5 Apr 2016 12:09:19 -0700 Subject: [PATCH 427/618] Update shades of red --- app/assets/stylesheets/framework/variables.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 98fe794d36..dd189a2cb7 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -104,9 +104,9 @@ $orange-light: rgba(252, 109, 38, 0.80); $orange-normal: #e75e40; $orange-dark: #ce5237; -$red-light: #f06559; -$red-normal: #e52c5a; -$red-dark: #d22852; +$red-light: #e52c5a; +$red-normal: #d22852; +$red-dark: darken($red-normal, 5%); $border-white-light: #f1f2f4; $border-white-normal: #d6dae2; @@ -128,9 +128,9 @@ $border-orange-light: #fc6d26; $border-orange-normal: #ce5237; $border-orange-dark: #c14e35; -$border-red-light: #f24f41; -$border-red-normal: #d22852; -$border-red-dark: #ca264f; +$border-red-light: #d22852; +$border-red-normal: #ca264f; +$border-red-dark: darken($border-red-normal, 5%); $help-well-bg: #fafafa; $help-well-border: #e5e5e5; From 5ee6badade3c453c7090e9c1f1f4d636c5bb068e Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Tue, 5 Apr 2016 16:33:37 -0300 Subject: [PATCH 428/618] Unblocks user when active_directory is disabled and it can be found --- lib/gitlab/ldap/access.rb | 5 ++++- spec/lib/gitlab/ldap/access_spec.rb | 27 ++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb index da4435c730..f2b649e50a 100644 --- a/lib/gitlab/ldap/access.rb +++ b/lib/gitlab/ldap/access.rb @@ -33,7 +33,10 @@ module Gitlab def allowed? if ldap_user - return true unless ldap_config.active_directory + unless ldap_config.active_directory + user.activate if user.ldap_blocked? + return true + end # Block user in GitLab if he/she was blocked in AD if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter) diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 32a19bf344..f5b66b8156 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } - it 'should block user in GitLab' do + it 'blocks user in GitLab' do access.allowed? expect(user).to be_blocked expect(user).to be_ldap_blocked @@ -78,6 +78,31 @@ describe Gitlab::LDAP::Access, lib: true do end it { is_expected.to be_truthy } + + context 'when user cannot be found' do + before do + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil) + end + + it { is_expected.to be_falsey } + + it 'blocks user in GitLab' do + access.allowed? + expect(user).to be_blocked + expect(user).to be_ldap_blocked + end + end + + context 'when user was previously ldap_blocked' do + before do + user.ldap_block + end + + it 'unblocks the user if it exists' do + access.allowed? + expect(user).not_to be_blocked + end + end end end end From 1eeec7c63ee3a293f134ff16b5f2209e77ab0f6c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 5 Apr 2016 13:13:49 -0700 Subject: [PATCH 429/618] Update issue_spec test --- spec/javascripts/issue_spec.js.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 86ba9dd8e9..ea27f36e9b 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -29,8 +29,8 @@ describe 'reopen/close issue', -> spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://gitlab.com/issues/6/close') - req.success saved: true - + req.success id: 34 + $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') expect($btnReopen).toBeHidden() @@ -94,7 +94,7 @@ describe 'reopen/close issue', -> spyOn(jQuery, 'ajax').and.callFake (req) -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://gitlab.com/issues/6/reopen') - req.success saved: true + req.success id: 34 $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') From ee1de011d1a9e2990fd56a794bbff6b0ec374d00 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Tue, 5 Apr 2016 17:03:34 -0400 Subject: [PATCH 430/618] Premailer shouldn't remove script tags from our emails Closes #14943. --- config/initializers/premailer.rb | 3 ++- spec/mailers/shared/notify.rb | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb index a44316bc3a..b9176688bc 100644 --- a/config/initializers/premailer.rb +++ b/config/initializers/premailer.rb @@ -3,5 +3,6 @@ Premailer::Rails.config.merge!( generate_text_part: false, preserve_styles: true, remove_comments: true, - remove_ids: true + remove_ids: true, + remove_scripts: false ) diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb index 56a6dbf96f..5a85cb501d 100644 --- a/spec/mailers/shared/notify.rb +++ b/spec/mailers/shared/notify.rb @@ -141,10 +141,12 @@ shared_examples 'a new user email' do end shared_examples 'it should have Gmail Actions links' do + it { is_expected.to have_body_text '