diff options
25 files changed, 550 insertions, 115 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 38cdc7b9fba..a72beb42646 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -58,6 +58,8 @@ shortcut_handler = new ShortcutsNavigation(); new GLForm($('.issue-form')); new IssuableForm($('.issue-form')); + new LabelsSelect(); + new MilestoneSelect(); new IssuableTemplateSelectors(); break; case 'projects:merge_requests:new': @@ -66,6 +68,8 @@ shortcut_handler = new ShortcutsNavigation(); new GLForm($('.merge-request-form')); new IssuableForm($('.merge-request-form')); + new LabelsSelect(); + new MilestoneSelect(); new IssuableTemplateSelectors(); break; case 'projects:tags:new': diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js index d0305c6c6a1..86ecad0b1fc 100644 --- a/app/assets/javascripts/issuable.js +++ b/app/assets/javascripts/issuable.js @@ -41,7 +41,6 @@ return this.value === $button.data('label'); }).remove(); Issuable.filterResults($('.filter-form')); - return $('.js-label-select').trigger('update.label'); }); }, filterResults: (function(_this) { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index bab23ff5ac0..9d40a303a8f 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,8 +4,9 @@ var _this; _this = this; $('.js-label-select').each(function(i, dropdown) { - var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip; + var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, $toggleText, fieldName, useId, propertyName; $dropdown = $(dropdown); + $toggleText = $dropdown.find('.dropdown-toggle-text'); projectId = $dropdown.data('project-id'); labelUrl = $dropdown.data('labels'); issueUpdateURL = $dropdown.data('issueUpdate'); @@ -24,6 +25,10 @@ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip'); $value = $block.find('.value'); $loading = $block.find('.block-loading').fadeOut(); + fieldName = $dropdown.data('field-name'); + useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown'); + propertyName = useId ? 'id' : 'title'; + if (issueUpdateURL != null) { issueURLSplit = issueUpdateURL.split('/'); } @@ -40,7 +45,7 @@ saveLabelData = function() { var data, selected; - selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() { + selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() { return this.value; }).get(); data = {}; @@ -124,20 +129,22 @@ }; }).value(); if ($dropdown.hasClass('js-extra-options')) { + var extraData = []; if (showNo) { - data.unshift({ + extraData.unshift({ id: 0, title: 'No Label' }); } if (showAny) { - data.unshift({ + extraData.unshift({ isAny: true, title: 'Any Label' }); } - if (data.length > 2) { - data.splice(2, 0, 'divider'); + if (extraData.length) { + extraData.push('divider'); + data = extraData.concat(data); } } return callback(data); @@ -148,7 +155,7 @@ $li = $('<li>'); $a = $('<a href="#">'); selectedClass = []; - removesAll = label.id === 0 || (label.id == null); + removesAll = label.id <= 0 || (label.id == null); if ($dropdown.hasClass('js-filter-bulk-update')) { indeterminate = instance.indeterminateIds; active = instance.activeIds; @@ -205,27 +212,36 @@ }, selectable: true, filterable: true, + selected: $dropdown.data('selected') || [], toggleLabel: function(selected, el) { - var selected_labels; - selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active'); - if (selected && (selected.title != null)) { - if (selected_labels.length > 1) { - return selected.title + " +" + (selected_labels.length - 1) + " more"; - } else { - return selected.title; - } - } else if (!selected && selected_labels.length !== 0) { - if (selected_labels.length > 1) { - return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more"; - } else if (selected_labels.length === 1) { - return $(selected_labels).text(); - } + var isSelected = el !== null ? el.hasClass('is-active') : false, + title = selected.title; + + if (isSelected) { + this.selected.push(title); + } else { + var index = this.selected.indexOf(title); + this.selected.splice(index, 1); + } + + var selectedLabels = this.selected; + + if (selectedLabels.length === 1) { + return selectedLabels; + } else if (selectedLabels.length > 1) { + return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more"; } else { return defaultLabel; } }, fieldName: $dropdown.data('field-name'), id: function(label) { + if (label.id <= 0) return; + + if ($dropdown.hasClass('js-issuable-form-dropdown')) { + return label.id; + } + if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) { return label.title; } else { @@ -239,6 +255,11 @@ isMRIndex = page === 'projects:merge_requests:index'; $selectbox.hide(); $value.removeAttr('style'); + + if ($dropdown.hasClass('js-issuable-form-dropdown')) { + return; + } + if (page === 'projects:boards:show') { return; } @@ -264,9 +285,17 @@ clicked: function(label, $el, e) { var isIssueIndex, isMRIndex, page; _this.enableBulkLabelDropdown(); - if ($dropdown.hasClass('js-filter-bulk-update')) { + + if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { + $dropdown.parent() + .find('.dropdown-clear-active') + .removeClass('is-active') + } + + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { return; } + page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = page === 'projects:merge_requests:index'; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index e897ebdf630..260db626c96 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -58,7 +58,7 @@ title: 'Upcoming' }); } - if (extraOptions.length > 2) { + if (extraOptions.length) { extraOptions.push('divider'); } return callback(extraOptions.concat(data)); @@ -69,19 +69,20 @@ fields: ['title'] }, selectable: true, - toggleLabel: function(selected) { - if (selected && 'id' in selected) { + toggleLabel: function(selected, el, e) { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { return selected.title; } else { return defaultLabel; } }, + defaultLabel: defaultLabel, fieldName: $dropdown.data('field-name'), text: function(milestone) { return _.escape(milestone.title); }, id: function(milestone) { - if (!useId) { + if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) { return milestone.name; } else { return milestone.id; @@ -99,7 +100,8 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); - if ($dropdown.hasClass('js-filter-bulk-update')) { + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); return; } if (page === 'projects:boards:show') { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index bad82868ab0..b76a0831cf9 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -125,8 +125,8 @@ }, selectable: true, fieldName: $dropdown.data('field-name'), - toggleLabel: function(selected) { - if (selected && 'id' in selected) { + toggleLabel: function(selected, el) { + if (selected && 'id' in selected && $(el).hasClass('is-active')) { if (selected.text) { return selected.text; } else { @@ -136,6 +136,7 @@ return defaultLabel; } }, + defaultLabel: defaultLabel, inputId: 'issue_assignee_id', hidden: function(e) { $selectbox.hide(); @@ -146,7 +147,9 @@ page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); - if ($dropdown.hasClass('js-filter-bulk-update')) { + if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + e.preventDefault(); + selectedId = user.id; return; } if (page === 'projects:boards:show') { @@ -164,6 +167,9 @@ return assignTo(selected); } }, + id: function (user) { + return user.id; + }, renderRow: function(user) { var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username; username = user.username ? "@" + user.username : ""; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index b0ba112476b..4a87a73a68a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -604,3 +604,9 @@ display: block; color: $gl-placeholder-color; } + +.dropdown-toggle-text { + &.is-default { + color: $gl-placeholder-color; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 7fdd79fa8b9..ecfed2c32f1 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -353,6 +353,10 @@ .issuable-form-select-holder { display: inline-block; width: 250px; + + .dropdown-menu-toggle { + width: 100%; + } } .table-holder { diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 33206717089..0035633b774 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,4 +1,6 @@ class Projects::BoardsController < Projects::ApplicationController + include IssuableCollections + respond_to :html before_action :authorize_read_board!, only: [:show] diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 4566f3782cc..81e0b6bb5ae 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -40,8 +40,9 @@ module DropdownsHelper end def dropdown_toggle(toggle_text, data_attr, options = {}) + default_label = data_attr[:default_label] 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 = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") output << icon('chevron-down') output.html_safe end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5c04bba323f..d1e432e41ae 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -8,18 +8,12 @@ module IssuablesHelper end def multi_label_name(current_labels, default_label) - # current_labels may be a string from before - if current_labels.is_a?(Array) - if current_labels.count > 1 - "#{current_labels[0]} +#{current_labels.count - 1} more" + if current_labels && current_labels.any? + title = current_labels.first.try(:title) + if current_labels.size > 1 + "#{title} +#{current_labels.size - 1} more" else - current_labels[0] - end - elsif current_labels.is_a?(String) - if current_labels.nil? || current_labels.empty? - default_label - else - current_labels + title end else default_label diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 5e9f5837101..b9f3d6c75c2 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -115,8 +115,9 @@ module LabelsHelper end def labels_filter_path - if @project - namespace_project_labels_path(@project.namespace, @project, :json) + project = @target_project || @project + if project + namespace_project_labels_path(project.namespace, project, :json) else dashboard_labels_path(:json) end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index b3e6e468ecd..e3d6fbd06ef 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -47,8 +47,9 @@ module MilestonesHelper end def milestones_filter_dropdown_path - if @project - namespace_project_milestones_path(@project.namespace, @project, :json) + project = @target_project || @project + if project + namespace_project_milestones_path(project.namespace, project, :json) else dashboard_milestones_path(:json) end diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0f4f744a71f..a3bad0e8326 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,3 +1,4 @@ +- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder .issues-filters .issues-details-filters.row-content-block.second-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do @@ -12,19 +13,19 @@ - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "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 js-filter-submit", - 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", default_label: "Author" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id].present? = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { any_user: "Any Assignee", 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", default_label: "Assignee" } }) + placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter - = render "shared/issuable/milestone_dropdown" + = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown" + = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" } .pull-right - if controller.controller_name == 'boards' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 3856a4917b4..ac593d8937a 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,3 +1,4 @@ +- project = @target_project || @project = form_errors(issuable) - if @conflict @@ -82,38 +83,22 @@ = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } .issuable-form-select-holder - = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", - placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project, - first_user: true, current_user: true, include_blank: true) - %div - = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline' + - if issuable.assignee_id + = f.hidden_field :assignee_id + = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", + placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee" } }) .form-group.issue-milestone = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" .col-sm-10{ class: ("col-lg-8" if has_due_date) } - - if milestone_options(issuable).present? - .issuable-form-select-holder - = f.select(:milestone_id, milestone_options(issuable), - { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) - - else - .prepend-top-10 - %span.light No open milestones available. - - if can? current_user, :admin_milestone, issuable.project - %div - = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .issuable-form-select-holder + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input" .form-group - has_labels = issuable.project.labels.any? = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" + = f.hidden_field :label_ids, multiple: true, value: '' .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } - - if has_labels - .issuable-form-select-holder - = f.collection_select :label_ids, issuable.project.labels.all, :id, :name, - { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" } - - else - %span.light No labels yet. - - if can? current_user, :admin_label, issuable.project - %div - = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline" + .issuable-form-select-holder + = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false } - if has_due_date .col-lg-6 .form-group diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 24a1a616919..6d5bdbe435e 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -1,25 +1,28 @@ +- project = @target_project || @project - show_create = local_assigns.fetch(:show_create, true) - extra_options = local_assigns.fetch(:extra_options, true) - filter_submit = local_assigns.fetch(:filter_submit, true) - show_footer = local_assigns.fetch(:show_footer, true) +- use_id = local_assigns.fetch(:use_id, true) - data_options = local_assigns.fetch(:data_options, {}) - classes = local_assigns.fetch(:classes, []) -- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"} +- selected = local_assigns.fetch(:selected, nil) +- selected_toggle = local_assigns.fetch(:selected_toggle, nil) +- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", project_id: project.try(:id), labels: labels_filter_path, default_label: "Labels"} - dropdown_data.merge!(data_options) - classes << 'js-extra-options' if extra_options - classes << 'js-filter-submit' if filter_submit -- if params[:label_name].present? - - if params[:label_name].respond_to?('any?') - - params[:label_name].each do |label| - = hidden_field_tag "label_name[]", u(label), id: nil +- if selected + - selected.each do |label| + = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : u(label.try(:title)), id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data} - %span.dropdown-toggle-text - = h(multi_label_name(params[:label_name], "Label")) + %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) } + = multi_label_name(selected, "Labels") = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create } - - if show_create and @project and can?(current_user, :admin_label, @project) + - if show_create && project && can?(current_user, :admin_label, project) = render partial: "shared/issuable/label_page_create" = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 2fcf40ece99..ab537dc0ed7 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,16 +1,19 @@ -- if params[:milestone_title].present? - = hidden_field_tag(:milestone_title, params[:milestone_title]) -= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), 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: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - - if @project +- project = @target_project || @project +- extra_class = extra_class || '' +- selected_text = selected.try(:title) +- if selected.present? + = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id) += dropdown_tag(milestone_dropdown_label(selected_text), options: { title: "Filter by milestone", toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do + - if project %ul.dropdown-footer-list - - if can? current_user, :admin_milestone, @project + - if can? current_user, :admin_milestone, project %li - = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do + = 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 + = link_to namespace_project_milestones_path(project.namespace, project) do + - if can? current_user, :admin_milestone, project Manage milestones - else View milestones diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b13daaf43c9..4e3bb8a8285 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -108,29 +108,30 @@ .js-due-date-calendar - if issuable.project.labels.any? + - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } = icon('tags') %span - = issuable.labels_array.size + = selected_labels.size .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } - - if issuable.labels_array.any? - - issuable.labels_array.each do |label| + .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } + - if selected_labels.any? + - selected_labels.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else %span.no-value None .selectbox.hide-collapsed - - issuable.labels_array.each do |label| + - selected_labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} - %span.dropdown-toggle-text - Label + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} + %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?)} + = multi_label_name(selected_labels, "Labels") = icon('chevron-down') .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default" diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature index 358e622b736..80670063ea0 100644 --- a/features/project/issues/issues.feature +++ b/features/project/issues/issues.feature @@ -37,6 +37,7 @@ Feature: Project Issues And I submit new issue "500 error on profile" Then I should see issue "500 error on profile" + @javascript Scenario: I submit new unassigned issue with labels Given project "Shop" has labels: "bug", "feature", "enhancement" And I click link "New Issue" diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index dacab6c7977..6c14d835004 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -138,19 +138,19 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I click "Assign to" dropdown"' do - first('.ajax-users-select').click + click_button 'Assignee' end step 'I should see the target project ID in the input selector' do - expect(page).to have_selector("input[data-project-id=\"#{@project.id}\"]") + expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}" end step 'I should see the users from the target project ID' do - expect(page).to have_selector('.user-result', visible: true, count: 3) - users = page.all('.user-name') - expect(users[0].text).to eq 'Unassigned' - expect(users[1].text).to eq current_user.name - expect(users[2].text).to eq @project.users.first.name + page.within '.dropdown-menu-user' do + expect(page).to have_content 'Unassigned' + expect(page).to have_content current_user.name + expect(page).to have_content @project.users.first.name + end end # Verify a link is generated against the correct project diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb index e21f76d00d9..99bd2ac3bc0 100644 --- a/features/steps/project/issues/issues.rb +++ b/features/steps/project/issues/issues.rb @@ -84,7 +84,8 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps step 'I submit new issue "500 error on profile" with label \'bug\'' do fill_in "issue_title", with: "500 error on profile" - select 'bug', from: "Labels" + click_button "Label" + click_link "bug" click_button "Submit issue" end diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb index 0e9f814044e..372ad39676d 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/issues/filter_issues_spec.rb @@ -96,9 +96,9 @@ describe 'Filter issues', feature: true do wait_for_ajax page.within '.labels-filter' do - expect(page).to have_content 'No Label' + expect(page).to have_content 'Labels' end - expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label') + expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels') end it 'filters by no label' do diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb new file mode 100644 index 00000000000..8771cc8e157 --- /dev/null +++ b/spec/features/issues/form_spec.rb @@ -0,0 +1,119 @@ +require 'rails_helper' + +describe 'New/edit issue', feature: true, js: true do + let!(:project) { create(:project) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:label2) { create(:label, project: project) } + let!(:issue) { create(:issue, project: project, assignee: user, milestone: milestone) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'new issue' do + before do + visit new_namespace_project_issue_path(project.namespace, project) + end + + it 'allows user to create new issue' do + fill_in 'issue_title', with: 'title' + fill_in 'issue_description', with: 'title' + + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Submit issue' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + + context 'edit issue' do + before do + visit edit_namespace_project_issue_path(project.namespace, project, issue) + end + + it 'allows user to update issue' do + expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s) + expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + + page.within '.js-user-search' do + expect(page).to have_content user.name + end + + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end +end diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 7773c486b4e..055210399a7 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -55,7 +55,7 @@ feature 'issue move to another project' do first('.select2-choice').click end - fill_in('s2id_autogen2_search', with: new_project_search.name) + fill_in('s2id_autogen1_search', with: new_project_search.name) page.within '.select2-drop' do expect(page).to have_content(new_project_search.name) diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d744d0eb6af..36ef28ae992 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -51,9 +51,8 @@ describe 'Issues', feature: true do expect(page).to have_content "Assignee #{@user.name}" - first('#s2id_issue_assignee_id').click - sleep 2 # wait for ajax stuff to complete - first('.user-result').click + first('.js-user-search').click + click_link 'Unassigned' click_button 'Save changes' diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb new file mode 100644 index 00000000000..7594cbf54e8 --- /dev/null +++ b/spec/features/merge_requests/form_spec.rb @@ -0,0 +1,273 @@ +require 'rails_helper' + +describe 'New/edit merge request', feature: true, js: true do + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let(:fork_project) { create(:project, forked_from_project: project) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:label2) { create(:label, project: project) } + + before do + project.team << [user, :master] + end + + context 'owned projects' do + before do + login_as(user) + end + + context 'new merge request' do + before do + visit new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'fix', + target_branch: 'master' + }) + end + + it 'creates new merge request' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Submit merge request' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + + context 'edit merge request' do + before do + merge_request = create(:merge_request, + source_project: project, + target_project: project, + source_branch: 'fix', + target_branch: 'master' + ) + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'updates merge request' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + page.within '.js-label-select' do + expect(page).to have_content label.title + end + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + end + + context 'forked project' do + before do + fork_project.team << [user, :master] + login_as(user) + end + + context 'new merge request' do + before do + visit new_namespace_project_merge_request_path( + fork_project.namespace, + fork_project, + merge_request: { + source_project_id: fork_project.id, + target_project_id: project.id, + source_branch: 'fix', + target_branch: 'master' + }) + end + + it 'creates new merge request' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + page.within '.js-label-select' do + expect(page).to have_content label.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + + click_button 'Submit merge request' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + + context 'edit merge request' do + before do + merge_request = create(:merge_request, + source_project: fork_project, + target_project: project, + source_branch: 'fix', + target_branch: 'master' + ) + + visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'should update merge request' do + click_button 'Assignee' + page.within '.dropdown-menu-user' do + click_link user.name + end + expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s) + page.within '.js-assignee-search' do + expect(page).to have_content user.name + end + + click_button 'Milestone' + page.within '.issue-milestone' do + click_link milestone.title + end + expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + end + + click_button 'Labels' + page.within '.dropdown-menu-labels' do + click_link label.title + click_link label2.title + end + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s) + expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s) + page.within '.js-label-select' do + expect(page).to have_content label.title + end + + click_button 'Save changes' + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content user.name + end + + page.within '.milestone' do + expect(page).to have_content milestone.title + end + + page.within '.labels' do + expect(page).to have_content label.title + expect(page).to have_content label2.title + end + end + end + end + end +end |