summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Schatz <jschatz1@gmail.com>2016-03-10 16:24:32 +0000
committerJacob Schatz <jschatz1@gmail.com>2016-03-10 16:24:32 +0000
commite0800285704800e33ead4a6317b2019873dac66b (patch)
treeea888af7e98e36ae45ed72b3c78376b1bf636bcc
parent3d4063efa1008d14fb35101916da70aa896887ed (diff)
parentee9a6d7fb3215f5ec66152aa64ed40a7ef61fb1b (diff)
downloadgitlab-ce-e0800285704800e33ead4a6317b2019873dac66b.tar.gz
Merge branch 'dropdown-js' into 'master'
Dropdown JS See merge request !3112
-rw-r--r--app/assets/javascripts/api.js.coffee14
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee270
-rw-r--r--app/assets/javascripts/issue_status_select.js.coffee11
-rw-r--r--app/assets/javascripts/labels_select.js.coffee92
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee60
-rw-r--r--app/assets/javascripts/users_select.js.coffee75
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss36
-rw-r--r--app/assets/stylesheets/framework/filters.scss1
-rw-r--r--app/assets/stylesheets/pages/labels.scss22
-rw-r--r--app/helpers/dropdowns_helper.rb100
-rw-r--r--app/views/help/ui.html.haml45
-rw-r--r--app/views/shared/issuable/_filter.html.haml91
-rw-r--r--features/steps/dashboard/issues.rb17
-rw-r--r--features/steps/dashboard/merge_requests.rb17
-rw-r--r--features/steps/project/issues/filter_labels.rb5
-rw-r--r--features/steps/project/issues/issues.rb11
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb7
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb7
18 files changed, 842 insertions, 39 deletions
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 3e0fdb3f795..2ddf8612db3 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -4,6 +4,7 @@
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"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -61,6 +62,19 @@
).done (projects) ->
callback(projects)
+ newLabel: (project_id, data, callback) ->
+ url = Api.buildUrl(Api.labels_path)
+ url = url.replace(':id', project_id)
+
+ data.private_token = gon.api_token
+ $.ajax(
+ url: url
+ type: "POST"
+ data: data
+ dataType: "json"
+ ).done (label) ->
+ callback(label)
+
# Return group projects list. Filtered by query
groupProjects: (group_id, query, callback) ->
url = Api.buildUrl(Api.group_projects_path)
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
new file mode 100644
index 00000000000..8e1449bc59c
--- /dev/null
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -0,0 +1,270 @@
+class GitLabDropdownFilter
+ BLUR_KEYCODES = [27, 40]
+
+ constructor: (@dropdown, @options) ->
+ @input = @dropdown.find(".dropdown-input .dropdown-input-field")
+
+ # Key events
+ timeout = ""
+ @input.on "keyup", (e) =>
+ if e.keyCode is 13 && @input.val() isnt ""
+ if @options.enterCallback
+ @options.enterCallback()
+ return
+
+ clearTimeout timeout
+ timeout = setTimeout =>
+ blur_field = @shouldBlur e.keyCode
+ search_text = @input.val()
+
+ if blur_field
+ @input.blur()
+
+ if @options.remote
+ @options.query search_text, (data) =>
+ @options.callback(data)
+ else
+ @filter search_text
+ , 250
+
+ shouldBlur: (keyCode) ->
+ return BLUR_KEYCODES.indexOf(keyCode) >= 0
+
+ filter: (search_text) ->
+ data = @options.data()
+ results = data
+
+ if search_text isnt ""
+ results = fuzzaldrinPlus.filter(data, search_text,
+ key: @options.keys
+ )
+
+ @options.callback results
+
+class GitLabDropdownRemote
+ constructor: (@dataEndpoint, @options) ->
+
+ execute: ->
+ if typeof @dataEndpoint is "string"
+ @fetchData()
+ else if typeof @dataEndpoint is "function"
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data by calling the data funcfion
+ @dataEndpoint "", (data) =>
+ if @options.success
+ @options.success(data)
+
+ if @options.beforeSend
+ @options.beforeSend()
+
+ # Fetch the data through ajax if the data is a string
+ fetchData: ->
+ $.ajax(
+ url: @dataEndpoint,
+ dataType: @options.dataType,
+ beforeSend: =>
+ if @options.beforeSend
+ @options.beforeSend()
+ success: (data) =>
+ if @options.success
+ @options.success(data)
+ )
+
+class GitLabDropdown
+ LOADING_CLASS = "is-loading"
+ PAGE_TWO_CLASS = "is-page-two"
+ ACTIVE_CLASS = "is-active"
+
+ constructor: (@el, @options) ->
+ self = @
+ @dropdown = $(@el).parent()
+ 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
+
+ @parseData @fullData
+ }
+
+ # Init filiterable
+ if @options.filterable
+ @filter = new GitLabDropdownFilter @dropdown,
+ remote: @options.filterRemote
+ query: @options.data
+ keys: @options.search.fields
+ data: =>
+ return @fullData
+ callback: (data) =>
+ @parseData data
+ enterCallback: =>
+ @selectFirstRow()
+
+ # Event listeners
+ @dropdown.on "shown.bs.dropdown", @opened
+ @dropdown.on "hidden.bs.dropdown", @hidden
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
+ e.preventDefault()
+ e.stopPropagation()
+
+ @togglePage()
+
+ if @options.selectable
+ selector = ".dropdown-content a"
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content a"
+
+ @dropdown.on "click", selector, (e) ->
+ self.rowClicked $(@)
+
+ if self.options.clicked
+ self.options.clicked()
+
+ toggleLoading: ->
+ $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
+
+ togglePage: ->
+ menu = $('.dropdown-menu', @dropdown)
+
+ if menu.hasClass(PAGE_TWO_CLASS)
+ if @remote
+ @remote.execute()
+
+ menu.toggleClass PAGE_TWO_CLASS
+
+ parseData: (data) ->
+ @renderedData = data
+
+ # Render each row
+ html = $.map data, (obj) =>
+ return @renderItem(obj)
+
+ if @options.filterable and data.length is 0
+ # render no matching results
+ html = [@noResults()]
+
+ # Render the full menu
+ full_html = @renderMenu(html.join(""))
+
+ @appendMenu(full_html)
+
+ opened: =>
+ contentHtml = $('.dropdown-content', @dropdown).html()
+ if @remote && contentHtml is ""
+ @remote.execute()
+
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").focus()
+
+ hidden: =>
+ if @options.filterable
+ @dropdown.find(".dropdown-input-field").blur().val("")
+
+ if @dropdown.find(".dropdown-toggle-page").length
+ $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
+
+
+ # Render the full menu
+ renderMenu: (html) ->
+ menu_html = ""
+
+ if @options.renderMenu
+ menu_html = @options.renderMenu(html)
+ else
+ menu_html = "<ul>#{html}</ul>"
+
+ return menu_html
+
+ # Append the menu into the dropdown
+ appendMenu: (html) ->
+ selector = '.dropdown-content'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content"
+
+ $(selector, @dropdown).html html
+
+ # Render the row
+ renderItem: (data) ->
+ html = ""
+
+ return "<li class='divider'></li>" if data is "divider"
+
+ if @options.renderRow
+ # Call the render function
+ html = @options.renderRow(data)
+ 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 ""
+ cssClass = "";
+
+ if selected
+ cssClass = "is-active"
+
+ html = "<li>"
+ html += "<a href='#{url}' class='#{cssClass}'>"
+ html += text
+ html += "</a>"
+ html += "</li>"
+
+ return html
+
+ noResults: ->
+ html = "<li>"
+ html += "<a href='#' class='is-focused'>"
+ html += "No matching results."
+ html += "</a>"
+ html += "</li>"
+
+ rowClicked: (el) ->
+ fieldName = @options.fieldName
+ field = @dropdown.parent().find("input[name='#{fieldName}']")
+
+ if el.hasClass(ACTIVE_CLASS)
+ field.remove()
+ else
+ fieldName = @options.fieldName
+ selectedIndex = el.parent().index()
+ if @renderedData
+ selectedObject = @renderedData[selectedIndex]
+ value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
+
+ if @options.multiSelect
+ oldValue = field.val()
+ if oldValue
+ value = "#{oldValue},#{value}"
+ else
+ @dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
+ field.remove()
+
+ # Toggle active class for the tick mark
+ el.toggleClass "is-active"
+
+ if value
+ if !field.length
+ # Create hidden input for form
+ input = "<input type='hidden' name='#{fieldName}' />"
+ @dropdown.before input
+
+ @dropdown.parent().find("input[name='#{fieldName}']").val value
+
+ selectFirstRow: ->
+ selector = '.dropdown-content li:first-child a'
+ if @dropdown.find(".dropdown-toggle-page").length
+ selector = ".dropdown-page-one .dropdown-content li:first-child a"
+
+ # similute a click on the first link
+ $(selector).trigger "click"
+
+$.fn.glDropdown = (opts) ->
+ return @.each ->
+ new GitLabDropdown @, opts
diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee
new file mode 100644
index 00000000000..c5740f27ddd
--- /dev/null
+++ b/app/assets/javascripts/issue_status_select.js.coffee
@@ -0,0 +1,11 @@
+class @IssueStatusSelect
+ constructor: ->
+ $('.js-issue-status').each (i, el) ->
+ fieldName = $(el).data("field-name")
+
+ $(el).glDropdown(
+ selectable: true
+ fieldName: fieldName
+ id: (obj, el) ->
+ $(el).data("id")
+ )
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
new file mode 100644
index 00000000000..5ade2cb66cb
--- /dev/null
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -0,0 +1,92 @@
+class @LabelsSelect
+ constructor: ->
+ $('.js-label-select').each (i, dropdown) ->
+ projectId = $(dropdown).data('project-id')
+ labelUrl = $(dropdown).data("labels")
+ selectedLabel = $(dropdown).data('selected')
+ if selectedLabel
+ selectedLabel = selectedLabel.split(",")
+ newLabelField = $('#new_label_name')
+ newColorField = $('#new_label_color')
+ showNo = $(dropdown).data('show-no')
+ showAny = $(dropdown).data('show-any')
+
+ if newLabelField.length
+ $('.suggest-colors-dropdown a').on "click", (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+ newColorField.val $(this).data("color")
+ $('.js-dropdown-label-color-preview')
+ .css 'background-color', $(this).data("color")
+ .addClass 'is-active'
+
+ $('.js-new-label-btn').on "click", (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+
+ if newLabelField.val() isnt "" && newColorField.val() isnt ""
+ $('.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()
+ $('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
+
+ $(dropdown).glDropdown(
+ data: (term, callback) ->
+ # We have to fetch the JS version of the labels list because there is no
+ # public facing JSON url for labels
+ $.ajax(
+ url: labelUrl
+ ).done (data) ->
+ html = $(data)
+ data = []
+ html.find('.label-row a').each ->
+ data.push(
+ title: $(@).text().trim()
+ )
+
+ if showNo
+ data.unshift(
+ id: "0"
+ title: 'No label'
+ )
+
+ if showAny
+ data.unshift(
+ title: 'Any label'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, "divider"
+
+ callback data
+ renderRow: (label) ->
+ if $.isArray(selectedLabel)
+ selected = ""
+ $.each selectedLabel, (i, selectedLbl) ->
+ selectedLbl = selectedLbl.trim()
+ if selected is "" && label.title is selectedLbl
+ selected = "is-active"
+ else
+ selected = if label.title is selectedLabel then "is-active" else ""
+
+ "<li>
+ <a href='#' class='#{selected}'>
+ #{label.title}
+ </a>
+ </li>"
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ fieldName: $(dropdown).data('field-name')
+ id: (label) ->
+ label.title
+ clicked: ->
+ if $(dropdown).hasClass "js-filter-submit"
+ $(dropdown).parents('form').submit()
+ )
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
new file mode 100644
index 00000000000..5e884454a65
--- /dev/null
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -0,0 +1,60 @@
+class @MilestoneSelect
+ constructor: ->
+ $('.js-milestone-select').each (i, dropdown) ->
+ projectId = $(dropdown).data('project-id')
+ milestonesUrl = $(dropdown).data('milestones')
+ selectedMilestone = $(dropdown).data('selected')
+ showNo = $(dropdown).data('show-no')
+ showAny = $(dropdown).data('show-any')
+ useId = $(dropdown).data('use-id')
+
+ $(dropdown).glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: milestonesUrl
+ ).done (data) ->
+ html = $(data)
+ data = []
+ html.find('.milestone strong a').each ->
+ link = $(@).attr("href").split("/")
+ data.push(
+ id: link[link.length - 1]
+ title: $(@).text().trim()
+ )
+
+ if showNo
+ data.unshift(
+ id: "0"
+ title: 'No Milestone'
+ )
+
+ if showAny
+ data.unshift(
+ title: 'Any Milestone'
+ )
+
+ if data.length > 2
+ data.splice 2, 0, "divider"
+
+ callback(data)
+ filterable: true
+ search:
+ fields: ['title']
+ selectable: true
+ fieldName: $(dropdown).data('field-name')
+ text: (milestone) ->
+ milestone.title
+ id: (milestone) ->
+ if !useId
+ if milestone.title isnt "Any milestone"
+ milestone.title
+ else
+ ""
+ else
+ milestone.id
+ isSelected: (milestone) ->
+ milestone.title is selectedMilestone
+ clicked: ->
+ if $(dropdown).hasClass "js-filter-submit"
+ $(dropdown).parents('form').submit()
+ )
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 9467011799f..987c6f4b8d2 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -3,6 +3,81 @@ class @UsersSelect
@usersPath = "/autocomplete/users.json"
@userPath = "/autocomplete/users/:id.json"
+ $('.js-user-search').each (i, dropdown) =>
+ @projectId = $(dropdown).data('project-id')
+ @showCurrentUser = $(dropdown).data('current-user')
+ showNullUser = $(dropdown).data('null-user')
+ showAnyUser = $(dropdown).data('any-user')
+ firstUser = $(dropdown).data('first-user')
+ selectedId = $(dropdown).data('selected')
+
+ $(dropdown).glDropdown(
+ data: (term, callback) =>
+ @users term, (users) =>
+ if term.length is 0
+ showDivider = 0
+
+ if firstUser
+ # Move current user to the front of the list
+ for obj, index in users
+ if obj.username == firstUser
+ users.splice(index, 1)
+ users.unshift(obj)
+ break
+
+ if showNullUser
+ showDivider += 1
+ users.unshift(
+ name: 'Unassigned',
+ id: 0
+ )
+
+ if showAnyUser
+ showDivider += 1
+ name = showAnyUser
+ name = 'Any User' if name == true
+ anyUser = {
+ name: name,
+ id: null
+ }
+ users.unshift(anyUser)
+
+ if showDivider
+ users.splice(showDivider, 0, "divider")
+
+ # Send the data back
+ callback users
+ filterable: true
+ filterRemote: true
+ search:
+ fields: ['name', 'username']
+ selectable: true
+ fieldName: $(dropdown).data('field-name')
+ clicked: ->
+ if $(dropdown).hasClass "js-filter-submit"
+ $(dropdown).parents('form').submit()
+ renderRow: (user) ->
+ username = if user.username then "@#{user.username}" else ""
+ avatar = if user.avatar_url then user.avatar_url else false
+ selected = if user.id is selectedId then "is-active" else ""
+ img = ""
+
+ if avatar
+ img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
+
+ "<li>
+ <a href='#' class='dropdown-menu-user-link #{selected}'>
+ #{img}
+ <strong class='dropdown-menu-user-full-name'>
+ #{user.name}
+ </strong>
+ <span class='dropdown-menu-user-username'>
+ #{username}
+ </span>
+ </a>
+ </li>"
+ )
+
$('.ajax-users-select').each (i, select) =>
@projectId = $(select).data('project-id')
@groupId = $(select).data('group-id')
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index e5e1c8310ed..009d621fc74 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -29,8 +29,8 @@
.dropdown-menu-toggle {
position: relative;
- min-width: 160px;
- padding: 5px 20px 5px 10px;
+ width: 160px;
+ padding: 6px 20px 6px 10px;
background-color: $dropdown-toggle-bg;
color: $dropdown-toggle-color;
font-size: 15px;
@@ -65,7 +65,7 @@
position: absolute;
top: 100%;
left: 0;
- z-index: 9999;
+ z-index: 9;
width: 240px;
margin-top: 2px;
margin-bottom: 0;
@@ -117,15 +117,19 @@
white-space: nowrap;
overflow: hidden;
- &:hover {
+ &:hover,
+ &:focus,
+ &.is-focused {
background-color: $dropdown-link-hover-bg;
text-decoration: none;
+ outline: 0;
}
}
}
.dropdown-menu-paging {
- .dropdown-page-two {
+ .dropdown-page-two,
+ .dropdown-menu-back {
display: none;
}
@@ -134,7 +138,8 @@
display: none;
}
- .dropdown-page-two {
+ .dropdown-page-two,
+ .dropdown-menu-back {
display: block;
}
}
@@ -157,6 +162,7 @@
.dropdown-menu-user-full-name {
display: block;
margin-bottom: 2px;
+ font-weight: 600;
line-height: 1;
text-overflow: ellipsis;
overflow: hidden;
@@ -232,6 +238,7 @@
font-size: 14px;
border: 0;
background: none;
+ outline: 0;
&:hover {
color: darken($dropdown-title-btn-color, 15%);
@@ -298,6 +305,14 @@
border-top: 1px solid $dropdown-divider-color;
}
+.dropdown-footer-list {
+ font-size: 14px;
+
+ a {
+ padding-left: 10px;
+ }
+}
+
.dropdown-loading {
position: absolute;
top: 0;
@@ -317,3 +332,12 @@
margin-left: -14px;
}
}
+
+.dropdown-menu-labels {
+ .label {
+ position: relative;
+ width: 30px;
+ margin-right: 5px;
+ text-indent: -99999px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index eab41628677..c431e2b0df3 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,5 +1,6 @@
.filter-item {
margin-right: 6px;
+ vertical-align: top;
}
@media (min-width: 800px) {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 1c78aafdb87..5ec0966194c 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -7,6 +7,28 @@
display: inline-block;
margin-right: 10px;
}
+
+ &.suggest-colors-dropdown {
+ margin-bottom: 5px;
+
+ a {
+ @include border-radius(0);
+ width: 36.7px;
+ margin-right: 0;
+ margin-bottom: -5px;
+ }
+ }
+}
+
+.dropdown-label-color-preview {
+ display: none;
+ margin-top: 5px;
+ width: 100%;
+ height: 25px;
+
+ &.is-active {
+ display: block;
+ }
}
.label-row {
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
new file mode 100644
index 00000000000..74f326e0b83
--- /dev/null
+++ b/app/helpers/dropdowns_helper.rb
@@ -0,0 +1,100 @@
+module DropdownsHelper
+ def dropdown_tag(toggle_text, options: {}, &block)
+ content_tag :div, class: "dropdown" do
+ data_attr = { toggle: "dropdown" }
+
+ if options.has_key?(:data)
+ data_attr = options[:data].merge(data_attr)
+ end
+
+ dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
+
+ dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
+ output = ""
+
+ if options.has_key?(:title)
+ output << dropdown_title(options[:title])
+ end
+
+ if options.has_key?(:filter)
+ output << dropdown_filter(options[:placeholder])
+ end
+
+ output << content_tag(:div, class: "dropdown-content") do
+ capture(&block) if block && !options.has_key?(:footer_content)
+ end
+
+ if block && options.has_key?(:footer_content)
+ output << content_tag(:div, class: "dropdown-footer") do
+ capture(&block)
+ end
+ end
+
+ output << dropdown_loading
+
+ output.html_safe
+ end
+
+ dropdown_output.html_safe
+ end
+ end
+
+ def dropdown_toggle(toggle_text, data_attr, options)
+ content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
+ output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
+ output << icon('chevron-down')
+ output.html_safe
+ end
+ end
+
+ def dropdown_title(title, back: false)
+ content_tag :div, class: "dropdown-title" do
+ title_output = ""
+
+ if back
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
+ icon('arrow-left')
+ end
+ end
+
+ title_output << content_tag(:span, title)
+
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
+ icon('times')
+ end
+
+ title_output.html_safe
+ end
+ end
+
+ 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.html_safe
+ end
+ end
+
+ def dropdown_content(&block)
+ content_tag(:div, class: "dropdown-content") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_footer(&block)
+ content_tag(:div, class: "dropdown-footer") do
+ if block
+ capture(&block)
+ end
+ end
+ end
+
+ def dropdown_loading
+ content_tag :div, class: "dropdown-loading" do
+ icon('spinner spin')
+ end
+ end
+end
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 0123834bc31..d084559abc3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -390,6 +390,51 @@
%button.btn.btn-primary
Create
+ .example
+ %div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times')
+ .dropdown-input
+ %input.dropdown-input-field{type: "search", placeholder: "Filter results"}
+ = icon('search')
+ .dropdown-content
+ .dropdown-loading
+ = icon('spinner spin')
+ :javascript
+ $('#js-project-dropdown').glDropdown({
+ data: function (term, callback) {
+ Api.projects(term, "last_activity_at", function (data) {
+ callback(data);
+ });
+ },
+ text: function (project) {
+ return project.name_with_namespace || project.name;
+ },
+ selectable: true,
+ fieldName: "author_id",
+ filterable: true,
+ search: {
+ fields: ['name_with_namespace']
+ },
+ id: function (data) {
+ return data.id;
+ },
+ isSelected: function (data) {
+ return data.id === 2;
+ }
+ })
+
+ .example
+ %div
+ = dropdown_tag("Projects", options: { title: "Go to project", filter: true, placeholder: "Filter projects" })
+
%h2#panels Panels
.row
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index e55159d996b..c3fbba2ba54 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -7,22 +7,77 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- = users_select_tag(:author_id, selected: params[:author_id],
- placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ - if params[:author_id]
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag("Author", options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author",
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } })
.filter-item.inline
- = users_select_tag(:assignee_id, selected: params[:assignee_id],
- placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
+ - if params[:assignee_id]
+ = hidden_field_tag(:assignee_id, params[:assignee_id])
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-filter-submit", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: "Search assignee", data: { any_user: "Any Author", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } })
.filter-item.inline.milestone-filter
- = select_tag('milestone_title', projects_milestones_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Milestone'})
+ - if params[:milestone_title]
+ = hidden_field_tag(:milestone_title, params[:milestone_title])
+ = dropdown_tag("Milestone", options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do
+ - if @project
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_milestone, @project
+ %li
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+ Create new
+ %li
+ = link_to namespace_project_milestones_path(@project.namespace, @project) do
+ - if can? current_user, :admin_milestone, @project
+ Manage milestones
+ - else
+ View milestones
.filter-item.inline.labels-filter
- = select_tag('label_name', projects_labels_options,
- class: 'select2 trigger-submit', include_blank: true,
- data: {placeholder: 'Label'})
+ - if params[:label_name]
+ = hidden_field_tag(:label_name, params[:label_name])
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}}
+ %span.dropdown-toggle-text
+ Label
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ = dropdown_title("Filter by label")
+ = dropdown_filter("Search labels")
+ = dropdown_content
+ - if @project
+ = dropdown_footer do
+ %ul.dropdown-footer-list
+ - if can? current_user, :admin_label, @project
+ %li
+ %a.dropdown-toggle-page{href: "#"}
+ Create new
+ %li
+ = link_to namespace_project_labels_path(@project.namespace, @project) do
+ - if can? current_user, :admin_label, @project
+ Manage labels
+ - else
+ View labels
+ - if can? current_user, :admin_label, @project
+ .dropdown-page-two
+ = dropdown_title("Create new label", back: true)
+ = dropdown_content do
+ %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
+ &nbsp
+ %button.btn.btn-primary.js-new-label-btn{type: "button"}
+ Create
+ = dropdown_loading
+ .dropdown-loading
+ = icon('spinner spin')
.pull-right
= render 'shared/sort_dropdown'
@@ -31,11 +86,18 @@
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
.filter-item.inline
- = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" })
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ %ul
+ %li
+ %a{href: "#", data: {id: "reopen"}} Open
+ %li
+ %a{href: "#", data: {id: "close"}} Closed
.filter-item.inline
- = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
+ = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", 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), current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
- = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
+ placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
@@ -47,6 +109,9 @@
:javascript
new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index cbe54e2dc79..d723300f485 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -36,13 +36,22 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- select2(current_user.id, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ execute_script('$("#assignee_id").val("")')
+ execute_script('$(".js-user-search").first().click()')
+ sleep 1
+ execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
+ sleep 1
end
step 'I click "All" link' do
- select2(nil, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ execute_script('$(".js-user-search").first().click()')
+ sleep 1
+ execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
+ sleep 1
+ execute_script('$(".js-user-search").eq(1).click()')
+ sleep 1
+ execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
+ sleep 1
end
def should_see(issue)
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 28c8c6b6015..7fc0e444e86 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -40,13 +40,22 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- select2(current_user.id, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ execute_script('$("#assignee_id").val("")')
+ execute_script('$(".js-user-search").first().click()')
+ sleep 0.5
+ execute_script("$('.dropdown-content li:contains(\"#{current_user.to_reference}\") a').click()")
+ sleep 2
end
step 'I click "All" link' do
- select2(nil, from: "#author_id")
- select2(nil, from: "#assignee_id")
+ execute_script('$(".js-user-search").first().click()')
+ sleep 0.5
+ execute_script('$(".js-user-search").first().parent().find("li a").first().click()')
+ sleep 2
+ execute_script('$(".js-user-search").eq(1).click()')
+ sleep 0.5
+ execute_script('$(".js-user-search").eq(1).parent().find("li a").first().click()')
+ sleep 2
end
def should_see(merge_request)
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
index 50bb32429b9..6d50501a722 100644
--- a/features/steps/project/issues/filter_labels.rb
+++ b/features/steps/project/issues/filter_labels.rb
@@ -29,7 +29,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end
step 'I click link "bug"' do
- select2('bug', from: "#label_name")
+ page.find('.js-label-select').click
+ sleep 0.5
+ execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
+ sleep 2
end
step 'I click link "feature"' do
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index d9842ccf95e..383ab4e14da 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -27,7 +27,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click link "Closed"' do
- click_link "Closed"
+ find('.issues-state-filters a', text: "Closed").click
end
step 'I click button "Unsubscribe"' do
@@ -63,14 +63,15 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click "author" dropdown' do
- first('#s2id_author_id').click
+ page.find('.js-author-search').click
+ sleep 1
end
step 'I see current user as the first user' do
- expect(page).to have_selector('.user-result', visible: true, count: 3)
- users = page.all('.user-name')
+ expect(page).to have_selector('.dropdown-content', visible: true)
+ users = page.all('.dropdown-menu-author .dropdown-content li a')
expect(users[0].text).to eq 'Any Author'
- expect(users[1].text).to eq current_user.name
+ expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}"
end
step 'I submit new issue "500 error on profile"' do
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 591866b40d4..f6e33f651c4 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Issue filtering by Milestone', feature: true do
- include Select2Helper
-
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -31,6 +29,9 @@ feature 'Issue filtering by Milestone', feature: true do
end
def filter_by_milestone(title)
- select2(title, from: '#milestone_title')
+ find(".js-milestone-select").click
+ sleep 0.5
+ find(".milestone-filter a", text: title).click
+ sleep 1
end
end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index f70214e1122..1b2fd1bab10 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -1,8 +1,6 @@
require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
- include Select2Helper
-
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
@@ -31,6 +29,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
end
def filter_by_milestone(title)
- select2(title, from: '#milestone_title')
+ find(".js-milestone-select").click
+ sleep 0.5
+ find(".milestone-filter a", text: title).click
+ sleep 1
end
end