From f28ca293b78405d798f1df78a39d549157a57c07 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 6 Sep 2016 15:49:49 +0300 Subject: Add bulk update support for merge requests list Signed-off-by: Dmitriy Zaporozhets --- app/assets/javascripts/dispatcher.js | 5 +- app/assets/javascripts/issuable.js.es6 | 2 +- app/assets/javascripts/issues-bulk-assignment.js | 4 +- app/assets/stylesheets/pages/issuable.scss | 15 ++ app/assets/stylesheets/pages/issues.scss | 11 - app/controllers/concerns/issuable_actions.rb | 31 +++ app/controllers/projects/issues_controller.rb | 26 -- app/services/issuable/bulk_update_service.rb | 26 ++ app/services/issues/bulk_update_service.rb | 25 -- app/views/projects/issues/_issue.html.haml | 4 +- app/views/projects/issues/index.html.haml | 4 +- .../merge_requests/_merge_request.html.haml | 4 + .../merge_requests/_merge_requests.html.haml | 2 +- app/views/projects/merge_requests/index.html.haml | 2 + app/views/shared/issuable/_filter.html.haml | 12 +- spec/services/issuable/bulk_update_service_spec.rb | 282 +++++++++++++++++++++ spec/services/issues/bulk_update_service_spec.rb | 282 --------------------- 17 files changed, 377 insertions(+), 360 deletions(-) create mode 100644 app/services/issuable/bulk_update_service.rb delete mode 100644 app/services/issues/bulk_update_service.rb create mode 100644 spec/services/issuable/bulk_update_service_spec.rb delete mode 100644 spec/services/issues/bulk_update_service_spec.rb diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 38cdc7b9fba..179d3bc38a5 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -23,6 +23,7 @@ case 'projects:boards:show': shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:merge_requests:index': case 'projects:issues:index': Issuable.init(); new IssuableBulkActions(); @@ -93,10 +94,6 @@ break; case "projects:merge_requests:conflicts": window.mcui = new MergeConflictResolver() - case 'projects:merge_requests:index': - shortcut_handler = new ShortcutsNavigation(); - Issuable.init(); - break; case 'dashboard:activity': new Activities(); break; diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 4006ac740b2..82c14ef0157 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -77,7 +77,7 @@ }, checkChanged: function() { const $checkedIssues = $('.selected_issue:checked'); - const $updateIssuesIds = $('#update_issues_ids'); + const $updateIssuesIds = $('#update_issuable_ids'); const $issuesOtherFilters = $('.issues-other-filters'); const $issuesBulkUpdate = $('.issues_bulk_update'); diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js index 98d3358ba92..8ca90490426 100644 --- a/app/assets/javascripts/issues-bulk-assignment.js +++ b/app/assets/javascripts/issues-bulk-assignment.js @@ -5,7 +5,7 @@ if (opts == null) { opts = {}; } - this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issues-list .issue'); + this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li'); this.form.data('bulkActions', this); this.willUpdateLabels = false; this.bindEvents(); @@ -106,7 +106,7 @@ state_event: this.form.find('input[name="update[state_event]"]').val(), assignee_id: this.form.find('input[name="update[assignee_id]"]').val(), milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), - issues_ids: this.form.find('input[name="update[issues_ids]"]').val(), + issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), subscription_event: this.form.find('input[name="update[subscription_event]"]').val(), add_label_ids: [], remove_label_ids: [] diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 46c4a11aa2e..02d6d2082f9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -404,3 +404,18 @@ margin-bottom: $gl-padding; } } + +.issuable-list { + li { + .issue-check { + float: left; + padding-right: 16px; + margin-bottom: 10px; + min-width: 15px; + + .selected_issue { + vertical-align: text-top; + } + } + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index d14224ed00f..7a26b7ad497 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -7,17 +7,6 @@ margin-bottom: 2px; } - .issue-check { - float: left; - padding-right: 16px; - margin-bottom: 10px; - min-width: 15px; - - .selected_issue { - vertical-align: text-top; - } - } - .issue-labels { display: inline-block; } diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index f40b62446e5..76f7f179008 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -3,6 +3,7 @@ module IssuableActions included do before_action :authorize_destroy_issuable!, only: :destroy + before_action :authorize_admin_issuable!, only: :bulk_update end def destroy @@ -13,6 +14,13 @@ module IssuableActions redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]) end + def bulk_update + result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name) + quantity = result[:count] + + render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" } + end + private def authorize_destroy_issuable! @@ -20,4 +28,27 @@ module IssuableActions return access_denied! end end + + def authorize_admin_issuable! + unless current_user.can?(:"admin_#{resource_name}", @project) + return access_denied! + end + end + + def bulk_update_params + params.require(:update).permit( + :issuable_ids, + :assignee_id, + :milestone_id, + :state_event, + :subscription_event, + label_ids: [], + add_label_ids: [], + remove_label_ids: [] + ) + end + + def resource_name + @resource_name ||= controller_name.singularize + end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 72d2d361878..de02e28e384 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -20,9 +20,6 @@ class Projects::IssuesController < Projects::ApplicationController # Allow modify issue before_action :authorize_update_issue!, only: [:edit, :update] - # Allow issues bulk update - before_action :authorize_admin_issues!, only: [:bulk_update] - respond_to :html def index @@ -168,16 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController end end - def bulk_update - result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute - - respond_to do |format| - format.json do - render json: { notice: "#{result[:count]} issues updated" } - end - end - end - protected def issue @@ -237,17 +224,4 @@ class Projects::IssuesController < Projects::ApplicationController :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [] ) end - - def bulk_update_params - params.require(:update).permit( - :issues_ids, - :assignee_id, - :milestone_id, - :state_event, - :subscription_event, - label_ids: [], - add_label_ids: [], - remove_label_ids: [] - ) - end end diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb new file mode 100644 index 00000000000..60891cbb255 --- /dev/null +++ b/app/services/issuable/bulk_update_service.rb @@ -0,0 +1,26 @@ +module Issuable + class BulkUpdateService < IssuableBaseService + def execute(type) + model_class = type.classify.constantize + update_class = type.classify.pluralize.constantize::UpdateService + + ids = params.delete(:issuable_ids).split(",") + items = model_class.where(id: ids) + + %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key| + params.delete(key) unless params[key].present? + end + + items.each do |issuable| + next unless can?(current_user, :"update_#{type}", issuable) + + update_class.new(issuable.project, current_user, params).execute(issuable) + end + + { + count: items.count, + success: !items.count.zero? + } + end + end +end diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb deleted file mode 100644 index 7e19a73f71a..00000000000 --- a/app/services/issues/bulk_update_service.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Issues - class BulkUpdateService < BaseService - def execute - issues_ids = params.delete(:issues_ids).split(",") - issue_params = params - - %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key| - issue_params.delete(key) unless issue_params[key].present? - end - - issues = Issue.where(id: issues_ids) - - issues.each do |issue| - next unless can?(current_user, :update_issue, issue) - - Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) - end - - { - count: issues.count, - success: !issues.count.zero? - } - end - end -end diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 79b14819865..851d4c06990 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -1,7 +1,7 @@ %li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } } - - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) + - if @bulk_edit .issue-check - = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" + = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" .issue-title.title %span.issue-title-text diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 1a87045aa60..023ea5f17d7 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- @bulk_edit = can?(current_user, :admin_issue, @project) + - page_title "Issues" - new_issue_email = @project.new_issue_address(current_user) = render "projects/issues/head" @@ -29,7 +31,7 @@ New Issue = render 'shared/issuable/filter', type: :issues - .issues-holder + .issues-holder.issuable-list = render 'issues' - if new_issue_email = render 'issue_by_email', email: new_issue_email diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 5029b365f93..31f8d0aeb5b 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,4 +1,8 @@ %li{ class: mr_css_classes(merge_request) } + - if @bulk_edit + .issue-check + = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" + .merge-request-title.title %span.merge-request-title-text = link_to merge_request.title, merge_request_path(merge_request) diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 446887774a4..fe82f751f53 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -1,4 +1,4 @@ -%ul.content-list.mr-list +%ul.content-list.mr-list.issuable-list = render @merge_requests - if @merge_requests.blank? %li diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index ace275c689b..144b3a9c8c8 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -1,4 +1,6 @@ - @no_container = true +- @bulk_edit = can?(current_user, :admin_merge_request, @project) + - page_title "Merge Requests" = render "projects/issues/head" = render 'projects/last_push' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index fabf6d74392..9b5d1236e1d 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,9 +1,11 @@ +- boards_page = controller.controller_name == 'boards' + .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 - if params[:issue_search].present? = hidden_field_tag :issue_search, params[:issue_search] - - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) + - if @bulk_edit .check-all-holder = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" @@ -30,7 +32,7 @@ %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search])} Reset filters .pull-right - - if controller.controller_name == 'boards' + - if boards_page #js-boards-seach.issue-boards-search %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - if can?(current_user, :admin_list, @project) @@ -45,7 +47,7 @@ - else = render 'shared/sort_dropdown' - - if controller.controller_name == 'issues' + - if @bulk_edit .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do .filter-item.inline @@ -70,10 +72,10 @@ %li %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe - = hidden_field_tag 'update[issues_ids]', [] + = hidden_field_tag 'update[issuable_ids]', [] = hidden_field_tag :state_event, params[:state_event] .filter-item.inline - = button_tag "Update issues", class: "btn update_selected_issues btn-save" + = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" - if !@labels.nil? .row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb new file mode 100644 index 00000000000..6f7ce8ca992 --- /dev/null +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -0,0 +1,282 @@ +require 'spec_helper' + +describe Issuable::BulkUpdateService, services: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project, namespace: user.namespace) } + + def bulk_update(issues, extra_params = {}) + bulk_update_params = extra_params + .reverse_merge(issuable_ids: Array(issues).map(&:id).join(',')) + + Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue') + end + + describe 'close issues' do + let(:issues) { create_list(:issue, 2, project: project) } + + it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'close') + + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(issues.count) + end + + it 'closes all the issues passed' do + bulk_update(issues, state_event: 'close') + + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty + end + end + + describe 'reopen issues' do + let(:issues) { create_list(:closed_issue, 2, project: project) } + + it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'reopen') + + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(issues.count) + end + + it 'reopens all the issues passed' do + bulk_update(issues, state_event: 'reopen') + + expect(project.issues.closed).to be_empty + expect(project.issues.opened).not_to be_empty + end + end + + describe 'updating assignee' do + let(:issue) { create(:issue, project: project, assignee: user) } + + context 'when the new assignee ID is a valid user' do + it 'succeeds' do + result = bulk_update(issue, assignee_id: create(:user).id) + + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end + + it 'updates the assignee to the use ID passed' do + assignee = create(:user) + + expect { bulk_update(issue, assignee_id: assignee.id) } + .to change { issue.reload.assignee }.from(user).to(assignee) + end + end + + context 'when the new assignee ID is -1' do + it 'unassigns the issues' do + expect { bulk_update(issue, assignee_id: -1) } + .to change { issue.reload.assignee }.to(nil) + end + end + + context 'when the new assignee ID is not present' do + it 'does not unassign' do + expect { bulk_update(issue, assignee_id: nil) } + .not_to change { issue.reload.assignee } + end + end + end + + describe 'updating milestones' do + let(:issue) { create(:issue, project: project) } + let(:milestone) { create(:milestone, project: project) } + + it 'succeeds' do + result = bulk_update(issue, milestone_id: milestone.id) + + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end + + it 'updates the issue milestone' do + expect { bulk_update(issue, milestone_id: milestone.id) } + .to change { issue.reload.milestone }.from(nil).to(milestone) + end + end + + describe 'updating labels' do + def create_issue_with_labels(labels) + create(:labeled_issue, project: project, labels: labels) + end + + let(:bug) { create(:label, project: project) } + let(:regression) { create(:label, project: project) } + let(:merge_requests) { create(:label, project: project) } + + let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } + let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } + let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } + let(:issue_no_labels) { create(:issue, project: project) } + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } + + let(:labels) { [] } + let(:add_labels) { [] } + let(:remove_labels) { [] } + + let(:bulk_update_params) do + { + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), + remove_label_ids: remove_labels.map(&:id), + } + end + + before do + bulk_update(issues, bulk_update_params) + end + + context 'when label_ids are passed' do + let(:issues) { [issue_all_labels, issue_no_labels] } + let(:labels) { [bug, regression] } + + it 'updates the labels of all issues passed to the labels passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + + context 'when those label IDs are empty' do + let(:labels) { [] } + + it 'updates the issues passed to have no labels' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + end + end + + context 'when add_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug, regression, merge_requests] } + + it 'adds those label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:remove_labels) { [bug, regression, merge_requests] } + + it 'removes those label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + + context 'when add_label_ids and label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } + let(:labels) { [merge_requests] } + let(:add_labels) { [regression] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'does not update issues not passed in' do + expect(issue_no_labels.label_ids).to be_empty + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:issues) { [issue_no_labels, issue_bug_and_regression] } + let(:labels) { [merge_requests] } + let(:remove_labels) { [regression] } + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'does not update issues not passed in' do + expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) + end + end + + context 'when add_label_ids, remove_label_ids, and label_ids are passed' do + let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } + let(:labels) { [regression] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } + + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end + end + end + + describe 'subscribe to issues' do + let(:issues) { create_list(:issue, 2, project: project) } + + it 'subscribes the given user' do + bulk_update(issues, subscription_event: 'subscribe') + + expect(issues).to all(be_subscribed(user)) + end + end + + describe 'unsubscribe from issues' do + let(:issues) do + create_list(:closed_issue, 2, project: project) do |issue| + issue.subscriptions.create(user: user, subscribed: true) + end + end + + it 'unsubscribes the given user' do + bulk_update(issues, subscription_event: 'unsubscribe') + + issues.each do |issue| + expect(issue).not_to be_subscribed(user) + end + end + end +end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb deleted file mode 100644 index ac08aa53b0b..00000000000 --- a/spec/services/issues/bulk_update_service_spec.rb +++ /dev/null @@ -1,282 +0,0 @@ -require 'spec_helper' - -describe Issues::BulkUpdateService, services: true do - let(:user) { create(:user) } - let(:project) { create(:empty_project, namespace: user.namespace) } - - def bulk_update(issues, extra_params = {}) - bulk_update_params = extra_params - .reverse_merge(issues_ids: Array(issues).map(&:id).join(',')) - - Issues::BulkUpdateService.new(project, user, bulk_update_params).execute - end - - describe 'close issues' do - let(:issues) { create_list(:issue, 2, project: project) } - - it 'succeeds and returns the correct number of issues updated' do - result = bulk_update(issues, state_event: 'close') - - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(issues.count) - end - - it 'closes all the issues passed' do - bulk_update(issues, state_event: 'close') - - expect(project.issues.opened).to be_empty - expect(project.issues.closed).not_to be_empty - end - end - - describe 'reopen issues' do - let(:issues) { create_list(:closed_issue, 2, project: project) } - - it 'succeeds and returns the correct number of issues updated' do - result = bulk_update(issues, state_event: 'reopen') - - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(issues.count) - end - - it 'reopens all the issues passed' do - bulk_update(issues, state_event: 'reopen') - - expect(project.issues.closed).to be_empty - expect(project.issues.opened).not_to be_empty - end - end - - describe 'updating assignee' do - let(:issue) { create(:issue, project: project, assignee: user) } - - context 'when the new assignee ID is a valid user' do - it 'succeeds' do - result = bulk_update(issue, assignee_id: create(:user).id) - - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) - end - - it 'updates the assignee to the use ID passed' do - assignee = create(:user) - - expect { bulk_update(issue, assignee_id: assignee.id) } - .to change { issue.reload.assignee }.from(user).to(assignee) - end - end - - context 'when the new assignee ID is -1' do - it 'unassigns the issues' do - expect { bulk_update(issue, assignee_id: -1) } - .to change { issue.reload.assignee }.to(nil) - end - end - - context 'when the new assignee ID is not present' do - it 'does not unassign' do - expect { bulk_update(issue, assignee_id: nil) } - .not_to change { issue.reload.assignee } - end - end - end - - describe 'updating milestones' do - let(:issue) { create(:issue, project: project) } - let(:milestone) { create(:milestone, project: project) } - - it 'succeeds' do - result = bulk_update(issue, milestone_id: milestone.id) - - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) - end - - it 'updates the issue milestone' do - expect { bulk_update(issue, milestone_id: milestone.id) } - .to change { issue.reload.milestone }.from(nil).to(milestone) - end - end - - describe 'updating labels' do - def create_issue_with_labels(labels) - create(:labeled_issue, project: project, labels: labels) - end - - let(:bug) { create(:label, project: project) } - let(:regression) { create(:label, project: project) } - let(:merge_requests) { create(:label, project: project) } - - let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } - let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } - let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } - let(:issue_no_labels) { create(:issue, project: project) } - let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } - - let(:labels) { [] } - let(:add_labels) { [] } - let(:remove_labels) { [] } - - let(:bulk_update_params) do - { - label_ids: labels.map(&:id), - add_label_ids: add_labels.map(&:id), - remove_label_ids: remove_labels.map(&:id), - } - end - - before do - bulk_update(issues, bulk_update_params) - end - - context 'when label_ids are passed' do - let(:issues) { [issue_all_labels, issue_no_labels] } - let(:labels) { [bug, regression] } - - it 'updates the labels of all issues passed to the labels passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id))) - end - - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) - end - - context 'when those label IDs are empty' do - let(:labels) { [] } - - it 'updates the issues passed to have no labels' do - expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) - end - end - end - - context 'when add_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:add_labels) { [bug, regression, merge_requests] } - - it 'adds those label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) - end - - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) - end - end - - context 'when remove_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:remove_labels) { [bug, regression, merge_requests] } - - it 'removes those label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) - end - - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) - end - end - - context 'when add_label_ids and remove_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:add_labels) { [bug] } - let(:remove_labels) { [merge_requests] } - - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end - - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) - end - - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) - end - end - - context 'when add_label_ids and label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } - let(:labels) { [merge_requests] } - let(:add_labels) { [regression] } - - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) - end - - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end - - it 'does not update issues not passed in' do - expect(issue_no_labels.label_ids).to be_empty - end - end - - context 'when remove_label_ids and label_ids are passed' do - let(:issues) { [issue_no_labels, issue_bug_and_regression] } - let(:labels) { [merge_requests] } - let(:remove_labels) { [regression] } - - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) - end - - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) - end - - it 'does not update issues not passed in' do - expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) - end - end - - context 'when add_label_ids, remove_label_ids, and label_ids are passed' do - let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } - let(:labels) { [regression] } - let(:add_labels) { [bug] } - let(:remove_labels) { [merge_requests] } - - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end - - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) - end - - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) - end - - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) - end - end - end - - describe 'subscribe to issues' do - let(:issues) { create_list(:issue, 2, project: project) } - - it 'subscribes the given user' do - bulk_update(issues, subscription_event: 'subscribe') - - expect(issues).to all(be_subscribed(user)) - end - end - - describe 'unsubscribe from issues' do - let(:issues) do - create_list(:closed_issue, 2, project: project) do |issue| - issue.subscriptions.create(user: user, subscribed: true) - end - end - - it 'unsubscribes the given user' do - bulk_update(issues, subscription_event: 'unsubscribe') - - issues.each do |issue| - expect(issue).not_to be_subscribed(user) - end - end - end -end -- cgit v1.2.3