CE port for bulk updating group labels

- Original EE MR: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/14827
This commit is contained in:
Eugenia Grieff 2019-08-08 06:25:20 +00:00 committed by Mike Greiling
parent 3ad34c3a24
commit 01950c3944
11 changed files with 215 additions and 173 deletions

View file

@ -43,6 +43,7 @@ class Issue < ApplicationRecord
validates :project, presence: true
alias_attribute :parent_ids, :project_id
alias_method :issuing_parent, :project
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }

View file

@ -192,6 +192,7 @@ class MergeRequest < ApplicationRecord
alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id
alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
alias_method :issuing_parent, :target_project
def self.reference_prefix
'!'

View file

@ -29,7 +29,7 @@ module Issuable
items.each do |issuable|
next unless can?(current_user, :"update_#{type}", issuable)
update_class.new(issuable.project, current_user, params).execute(issuable)
update_class.new(issuable.issuing_parent, current_user, params).execute(issuable)
end
{

View file

@ -1,4 +1,4 @@
- @can_bulk_update = can?(current_user, :admin_issue, @group)
- @can_bulk_update = can?(current_user, :admin_issue, @group) && @group.feature_available?(:group_bulk_edit)
- page_title "Issues"
= content_for :meta_tags do

View file

@ -1,4 +1,4 @@
- @can_bulk_update = can?(current_user, :admin_merge_request, @group)
- @can_bulk_update = can?(current_user, :admin_merge_request, @group) && @group.feature_available?(:group_bulk_edit)
- page_title "Merge Requests"

View file

@ -1,4 +1,5 @@
- project = @target_project || @project
- edit_context = local_assigns.fetch(:edit_context, nil) || 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)
@ -8,7 +9,7 @@
- classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
- dropdown_data = label_dropdown_data(@project, labels: labels_filter_path_with_defaults, default_label: "Labels")
- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: "Labels")
- dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View file

@ -1,25 +1,31 @@
# Bulk editing issue and merge request milestones **(PREMIUM)**
# Bulk editing issues, merge requests, and epics at the group level **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7249) for issues in
[GitLab Premium](https://about.gitlab.com/pricing/) 12.1.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12719) for merge
requests in GitLab [GitLab Premium](https://about.gitlab.com/pricing/) 12.2.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7249) for issues in [GitLab Premium](https://about.gitlab.com/pricing/) 12.1.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12719) for merge requests in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7250) for epics in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2.
Milestones can be updated simultaneously across multiple issues or merge requests by using the bulk editing feature.
## Editing milestones and labels
> **Notes:**
>
> - A permission level of `Reporter` or higher is required in order to manage issues.
> - A permission level of `Developer` or higher is required in order to manage merge requests.
> - A permission level of `Reporter` or higher is required in order to manage epics.
By using the bulk editing feature:
- Milestones can be updated simultaneously across multiple issues or merge requests.
- Labels can be updated simultaneously across multiple issues, merge requests, or epics.
![Bulk editing](img/bulk-editing.png)
NOTE: **Note:**
A permission level of `Reporter` or higher is required in order to manage issues, and
a permission level of `Developer` or higher is required in order to manage merge requests.
To bulk update group issues, merge requests, or epics:
To bulk update group issue or merge request milestones:
1. Navigate to the issues or merge requests list.
1. Click the **Edit issues** or **Edit merge requests** button.
- This will open a sidebar on the right-hand side of your screen where an editable field
for milestones will be displayed.
- Checkboxes will also appear beside each issue or merge request.
1. Check the checkbox beside each issue to be edited.
1. Select the desired milestone from the sidebar.
1. Navigate to the issues, merge requests, or epics list.
1. Click **Edit issues**, **Edit merge requests**, or **Edit epics**.
- This will open a sidebar on the right-hand side where editable fields
for milestones and labels will be displayed.
- Checkboxes will also appear beside each issue, merge request, or epic.
1. Check the checkbox beside each issue, merge request, or epic to be edited.
1. Select the desired new values from the sidebar.
1. Click **Update all**.

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View file

@ -97,6 +97,22 @@ have a [start or due date](#start-date-and-due-date), then you can see a
Drag and drop to reorder issues and child epics. New issues and child epics added to an epic appear at the top of the list.
## Updating epics
### Using bulk editing
To apply labels across multiple epics:
1. Go to the Epics list.
1. Click **Edit epics**.
- Checkboxes will appear beside each epic.
- A sidebar on the right-hand side will appear, with an editable field for labels.
1. Check the checkbox beside each epic to be edited.
1. Select the desired labels.
1. Click **Update all**.
![bulk editing](img/bulk_editing.png)
## Deleting an epic
NOTE: **Note:**

View file

@ -31,7 +31,159 @@ describe Issuable::BulkUpdateService do
end
end
context 'with project issuables' do
shared_examples 'updating labels' do
def create_issue_with_labels(labels)
create(:labeled_issue, project: project, labels: labels)
end
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(match_array(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).flat_map(&:label_ids)).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).flat_map(&:label_ids)).not_to include(regression.id)
end
it 'ignores the label IDs parameter' do
expect(issues.map(&:reload).flat_map(&:label_ids)).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).flat_map(&:label_ids)).not_to include(merge_requests.id)
end
it 'ignores the label IDs parameter' do
expect(issues.map(&:reload).flat_map(&:label_ids)).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
context 'with issuables at a project level' do
describe 'close issues' do
let(:issues) { create_list(:issue, 2, project: project) }
@ -178,159 +330,11 @@ describe Issuable::BulkUpdateService do
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(match_array(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).flat_map(&:label_ids)).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).flat_map(&:label_ids)).not_to include(regression.id)
end
it 'ignores the label IDs parameter' do
expect(issues.map(&:reload).flat_map(&:label_ids)).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).flat_map(&:label_ids)).not_to include(merge_requests.id)
end
it 'ignores the label IDs parameter' do
expect(issues.map(&:reload).flat_map(&:label_ids)).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
it_behaves_like 'updating labels'
end
describe 'subscribe to issues' do
@ -360,7 +364,7 @@ describe Issuable::BulkUpdateService do
end
end
context 'with group issuables ' do
context 'with issuables at a group level' do
let(:group) { create(:group) }
describe 'updating milestones' do
@ -387,5 +391,18 @@ describe Issuable::BulkUpdateService do
it_behaves_like 'updates milestones'
end
end
describe 'updating labels' do
let(:project) { create(:project, :repository, group: group) }
let(:bug) { create(:group_label, group: group) }
let(:regression) { create(:group_label, group: group) }
let(:merge_requests) { create(:group_label, group: group) }
before do
group.add_reporter(user)
end
it_behaves_like 'updating labels'
end
end
end