From ab96ca2bf1ae72817ff5cedf1792c8f7563ebdef Mon Sep 17 00:00:00 2001
From: Alfredo Sumaran <alfredo@gitlab.com>
Date: Fri, 13 May 2016 10:57:03 -0500
Subject: [PATCH] Dropdown implementation

---
 CHANGELOG                                     |  3 +-
 app/assets/javascripts/api.js.coffee          | 34 ++++++-------
 .../blob/blob_gitignore_selector.js.coffee    | 51 +++++++++----------
 .../javascripts/blob/edit_blob.js.coffee      |  2 +-
 app/assets/javascripts/gl_dropdown.js.coffee  | 45 ++++++++++------
 .../javascripts/lib/type_utility.js.coffee    |  9 ++++
 app/helpers/blob_helper.rb                    | 10 ++--
 app/models/repository.rb                      |  6 ++-
 app/views/projects/blob/_editor.html.haml     |  2 +-
 lib/api/gitignores.rb                         | 10 ++--
 spec/javascripts/project_title_spec.js.coffee |  1 +
 11 files changed, 97 insertions(+), 76 deletions(-)
 create mode 100644 app/assets/javascripts/lib/type_utility.js.coffee

diff --git a/CHANGELOG b/CHANGELOG
index 683f1b1a650..e87fde3fece 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -66,13 +66,12 @@ v 8.8.0 (unreleased)
   - All Grape API helpers are now instrumented
   - Improve Issue formatting for the Slack Service (Jeroen van Baarsen)
   - Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine)
+  - When creating a .gitignore file a dropdown with templates will be provided
 
 v 8.7.6
   - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
   - Fix import from GitLab.com to a private instance failure. !4181
   - Fix external imports not finding the import data. !4106
-  - Fix import from gitlab.com to a private instance failure
-  - When creating a .gitignore file a dropdown with templates will be provided
 
 v 8.7.5
   - Fix relative links in wiki pages. !4050
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index c654f8de208..3f61ea1eaf4 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -1,15 +1,15 @@
 @Api =
-  groups_path: "/api/:version/groups.json"
-  group_path: "/api/:version/groups/:id.json"
-  namespaces_path: "/api/:version/namespaces.json"
-  group_projects_path: "/api/:version/groups/:id/projects.json"
-  projects_path: "/api/:version/projects.json"
-  labels_path: "/api/:version/projects/:id/labels"
-  license_path: "/api/:version/licenses/:key"
-  gitignoretext_path: "/api/:version/gitignores/:key"
+  groupsPath: "/api/:version/groups.json"
+  groupPath: "/api/:version/groups/:id.json"
+  namespacesPath: "/api/:version/namespaces.json"
+  groupProjectsPath: "/api/:version/groups/:id/projects.json"
+  projectsPath: "/api/:version/projects.json"
+  labelsPath: "/api/:version/projects/:id/labels"
+  licensePath: "/api/:version/licenses/:key"
+  gitignorePath: "/api/:version/gitignores/:key"
 
   group: (group_id, callback) ->
-    url = Api.buildUrl(Api.group_path)
+    url = Api.buildUrl(Api.groupPath)
     url = url.replace(':id', group_id)
 
     $.ajax(
@@ -23,7 +23,7 @@
   # Return groups list. Filtered by query
   # Only active groups retrieved
   groups: (query, skip_ldap, callback) ->
-    url = Api.buildUrl(Api.groups_path)
+    url = Api.buildUrl(Api.groupsPath)
 
     $.ajax(
       url: url
@@ -37,7 +37,7 @@
 
   # Return namespaces list. Filtered by query
   namespaces: (query, callback) ->
-    url = Api.buildUrl(Api.namespaces_path)
+    url = Api.buildUrl(Api.namespacesPath)
 
     $.ajax(
       url: url
@@ -51,7 +51,7 @@
 
   # Return projects list. Filtered by query
   projects: (query, order, callback) ->
-    url = Api.buildUrl(Api.projects_path)
+    url = Api.buildUrl(Api.projectsPath)
 
     $.ajax(
       url: url
@@ -65,7 +65,7 @@
       callback(projects)
 
   newLabel: (project_id, data, callback) ->
-    url = Api.buildUrl(Api.labels_path)
+    url = Api.buildUrl(Api.labelsPath)
     url = url.replace(':id', project_id)
 
     data.private_token = gon.api_token
@@ -81,7 +81,7 @@
 
   # Return group projects list. Filtered by query
   groupProjects: (group_id, query, callback) ->
-    url = Api.buildUrl(Api.group_projects_path)
+    url = Api.buildUrl(Api.groupProjectsPath)
     url = url.replace(':id', group_id)
 
     $.ajax(
@@ -96,7 +96,7 @@
 
   # Return text for a specific license
   licenseText: (key, data, callback) ->
-    url = Api.buildUrl(Api.license_path).replace(':key', key)
+    url = Api.buildUrl(Api.licensePath).replace(':key', key)
 
     $.ajax(
       url: url
@@ -104,8 +104,8 @@
     ).done (license) ->
       callback(license)
 
-  gitIgnoreText: (key, callback) ->
-    url = Api.buildUrl(Api.gitignoretext_path).replace(':key', key)
+  gitignoreText: (key, callback) ->
+    url = Api.buildUrl(Api.gitignorePath).replace(':key', key)
 
     $.get url, (gitignore) ->
       callback(gitignore)
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
index 2112f5c855a..cc8a497d081 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
@@ -1,10 +1,10 @@
-class @BlobGitIgnoreSelector
+class @BlobGitignoreSelector
   constructor: (opts) ->
     {
       @dropdown
       @editor
-      @wrapper        = @dropdown.parents('.gitignore-selector')
-      @fileNameInput  = $('#file_name')
+      @$wrapper        = @dropdown.closest('.gitignore-selector')
+      @$filenameInput  = $('#file_name')
       @data           = @dropdown.data('filenames')
     } = opts
 
@@ -13,51 +13,46 @@ class @BlobGitIgnoreSelector
       filterable: true,
       selectable: true,
       search:
-        fields: ['text']
-      clicked: @onClick.bind(@)
+        fields: ['name']
+      clicked: @onClick
+      text: (gitignore) ->
+        gitignore.name
     )
 
-    @toggleGitIgnoreSelector()
+    @toggleGitignoreSelector()
     @bindEvents()
 
   bindEvents: ->
-    @fileNameInput
+    @$filenameInput
       .on 'keyup blur', (e) =>
-        @toggleGitIgnoreSelector()
+        @toggleGitignoreSelector()
 
-  toggleGitIgnoreSelector: ->
-    filename = @fileNameInput.val() or $('.editor-file-name').text().trim()
-    @wrapper.toggleClass 'hidden', filename isnt '.gitignore'
+  toggleGitignoreSelector: ->
+    filename = @$filenameInput.val() or $('.editor-file-name').text().trim()
+    @$wrapper.toggleClass 'hidden', filename isnt '.gitignore'
 
-  onClick: (item, el, e) ->
+  onClick: (item, el, e) =>
     e.preventDefault()
-    @requestIgnoreFile(item.text)
+    @requestIgnoreFile(item.name)
 
   requestIgnoreFile: (name) ->
-    Api.gitIgnoreText name, @requestIgnoreFileSuccess.bind(@)
+    Api.gitignoreText name, @requestIgnoreFileSuccess.bind(@)
 
   requestIgnoreFileSuccess: (gitignore) ->
-    @editor.setValue(gitignore.content, -1)
-
-    # Move cursor position to end of file
-    row = @editor.session.getLength() - 1
-    column = @editor.session.getLine(row).length
-    @editor.gotoLine(row + 1, column)
+    @editor.setValue(gitignore.content, 1)
     @editor.focus()
 
-class @BlobGitIgnoreSelectors
+class @BlobGitignoreSelectors
   constructor: (opts) ->
-    _this = @
-
     {
-      @dropdowns = $('.js-gitignore-selector')
+      @$dropdowns = $('.js-gitignore-selector')
       @editor
     } = opts
 
-    @dropdowns.each ->
-      $dropdown = $(@)
+    @$dropdowns.each (i, dropdown) =>
+      $dropdown = $(dropdown)
 
-      new BlobGitIgnoreSelector(
+      new BlobGitignoreSelector(
         dropdown: $dropdown,
-        editor: _this.editor
+        editor: @editor
       )
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index f2efeb3960a..79141e768b8 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -13,7 +13,7 @@ class @EditBlob
 
     @initModePanesAndLinks()
     new BlobLicenseSelector(@editor)
-    new BlobGitIgnoreSelectors(editor: @editor)
+    new BlobGitignoreSelectors(editor: @editor)
 
   initModePanesAndLinks: ->
     @$editModePanes = $(".js-edit-mode-pane")
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 0f2ac664ac8..8cb31b2dbd7 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -60,23 +60,36 @@ class GitLabDropdownFilter
       results = data
 
       if search_text isnt ''
-        # handle groups
+        # When data is an array of objects therefore [object Array] e.g.
+        # [
+        #   { prop: 'foo' },
+        #   { prop: 'baz' }
+        # ]
         if _.isArray(data)
           results = fuzzaldrinPlus.filter(data, search_text,
             key: @options.keys
           )
-        else if _.isObject(data)
-          results = {}
-          for key, group of data
-            tmp = fuzzaldrinPlus.filter(group, search_text,
-              key: @options.keys
-            )
-
-            if tmp.length
-              results[key] = []
-
-              tmp.map (item) ->
-                results[key].push item
+        else
+          # If data is grouped therefore an [object Object]. e.g.
+          # {
+          #   groupName1: [
+          #     { prop: 'foo' },
+          #     { prop: 'baz' }
+          #   ],
+          #   groupName2: [
+          #     { prop: 'abc' },
+          #     { prop: 'def' }
+          #   ]
+          # }
+          if gl.utils.isObject data
+            results = {}
+            for key, group of data
+              tmp = fuzzaldrinPlus.filter(group, search_text,
+                key: @options.keys
+              )
+
+              if tmp.length
+                results[key] = tmp.map (item) -> item
 
       @options.callback results
     else
@@ -250,7 +263,7 @@ class GitLabDropdown
       html = [@noResults()]
     else
       # Handle array groups
-      if String(data) is "[object Object]"
+      if gl.utils.isObject data
         html = []
         for name, groupData of data
           # Add header for each group
@@ -269,7 +282,7 @@ class GitLabDropdown
     @appendMenu(full_html)
 
   renderData: (data, group = false) ->
-    $.map data, (obj, index) =>
+    data.map (obj, index) =>
       return @renderItem(obj, group, index)
 
   shouldPropagate: (e) =>
@@ -416,7 +429,7 @@ class GitLabDropdown
         selectedIndex = el.data('index')
         selectedObject = @renderedData[groupName][selectedIndex]
       else
-        selectedIndex = el.parent().index()
+        selectedIndex = el.closest('li').index()
         selectedObject = @renderedData[selectedIndex]
 
     value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
diff --git a/app/assets/javascripts/lib/type_utility.js.coffee b/app/assets/javascripts/lib/type_utility.js.coffee
new file mode 100644
index 00000000000..957f0d86b36
--- /dev/null
+++ b/app/assets/javascripts/lib/type_utility.js.coffee
@@ -0,0 +1,9 @@
+((w) ->
+
+  w.gl ?= {}
+  w.gl.utils ?= {}
+
+  w.gl.utils.isObject = (obj) ->
+    obj? and (obj.constructor is Object)
+
+) window
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index a3723d2c349..cec2dc753fe 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -185,13 +185,13 @@ module BlobHelper
     }
   end
 
-  def gitignores_for_select
-    return @gitignores_for_select if defined?(@gitignores_for_select)
+  def gitignore_names
+    return @gitignore_names if defined?(@gitignore_names)
 
-    @gitignores_for_select = {
-      Global: Gitlab::Gitignore.global.map{ |v| { text: v.name } },
+    @gitignore_names = {
+      Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } },
       # Note that the key here doesn't cover it really
-      Languages: Gitlab::Gitignore.languages_frameworks.map{ |v| { text: v.name } }
+      Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } }
     }
   end
 end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index f26278cc3af..ecc8795c954 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -245,7 +245,7 @@ class Repository
   def cache_keys
     %i(size branch_names tag_names commit_count
        readme version contribution_guide changelog
-       license_blob license_key)
+       license_blob license_key gitignore)
   end
 
   def build_cache
@@ -256,6 +256,10 @@ class Repository
     end
   end
 
+  def expire_gitignore
+    cache.expire(:gitignore)
+  end
+
   def expire_tags_cache
     cache.expire(:tag_names)
     @tags = nil
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 678eadafe15..4071b59c003 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -17,7 +17,7 @@
         = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name}
 
       .gitignore-selector.hidden
-        = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignores_for_select } } )
+        = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { filenames: gitignore_names } } )
 
       .encoding-selector
         = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb
index 1af9ba6b316..270c9501dd2 100644
--- a/lib/api/gitignores.rb
+++ b/lib/api/gitignores.rb
@@ -12,15 +12,15 @@ module API
     # Get the text for a specific gitignore
     #
     # Parameters:
-    #   key (required) - The key of a license
+    #   name (required) - The name of a license
     #
     # Example Request:
-    #   GET /gitignores/elixir
+    #   GET /gitignores/Elixir
     #
-    get 'gitignores/:key' do
-      required_attributes! [:key]
+    get 'gitignores/:name' do
+      required_attributes! [:name]
 
-      gitignore = Gitlab::Gitignore.find(params[:key])
+      gitignore = Gitlab::Gitignore.find(params[:name])
       not_found!('.gitignore') unless gitignore
 
       present gitignore, with: Entities::Gitignore
diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee
index 3d8de2ff989..1cf34d4d2d3 100644
--- a/spec/javascripts/project_title_spec.js.coffee
+++ b/spec/javascripts/project_title_spec.js.coffee
@@ -1,5 +1,6 @@
 #= require bootstrap
 #= require select2
+#= require lib/type_utility
 #= require gl_dropdown
 #= require api
 #= require project_select
-- 
GitLab