Commit 01950c39 authored by Eugenia Grieff's avatar Eugenia Grieff Committed by Mike Greiling

CE port for bulk updating group labels

- Original EE MR: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/14827
parent 3ad34c3a
...@@ -43,6 +43,7 @@ class Issue < ApplicationRecord ...@@ -43,6 +43,7 @@ class Issue < ApplicationRecord
validates :project, presence: true validates :project, presence: true
alias_attribute :parent_ids, :project_id alias_attribute :parent_ids, :project_id
alias_method :issuing_parent, :project
scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
......
...@@ -192,6 +192,7 @@ class MergeRequest < ApplicationRecord ...@@ -192,6 +192,7 @@ class MergeRequest < ApplicationRecord
alias_attribute :project, :target_project alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id alias_attribute :project_id, :target_project_id
alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
alias_method :issuing_parent, :target_project
def self.reference_prefix def self.reference_prefix
'!' '!'
......
...@@ -29,7 +29,7 @@ module Issuable ...@@ -29,7 +29,7 @@ module Issuable
items.each do |issuable| items.each do |issuable|
next unless can?(current_user, :"update_#{type}", 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 end
{ {
......
- @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" - page_title "Issues"
= content_for :meta_tags do = content_for :meta_tags do
......
- @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" - page_title "Merge Requests"
......
- project = @target_project || @project - project = @target_project || @project
- edit_context = local_assigns.fetch(:edit_context, nil) || project
- show_create = local_assigns.fetch(:show_create, true) - show_create = local_assigns.fetch(:show_create, true)
- extra_options = local_assigns.fetch(:extra_options, true) - extra_options = local_assigns.fetch(:extra_options, true)
- filter_submit = local_assigns.fetch(:filter_submit, true) - filter_submit = local_assigns.fetch(:filter_submit, true)
...@@ -8,7 +9,7 @@ ...@@ -8,7 +9,7 @@
- classes = local_assigns.fetch(:classes, []) - classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil) - selected = local_assigns.fetch(:selected, nil)
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") - 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) - dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels") - label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false) - no_default_styles = local_assigns.fetch(:no_default_styles, false)
......
# 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 > - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7249) for issues in [GitLab Premium](https://about.gitlab.com/pricing/) 12.1.
[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/12719) for merge > - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7250) for epics in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2.
requests in GitLab [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
![Bulk editing](img/bulk-editing.png) > **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:
NOTE: **Note:** - Milestones can be updated simultaneously across multiple issues or merge requests.
A permission level of `Reporter` or higher is required in order to manage issues, and - Labels can be updated simultaneously across multiple issues, merge requests, or epics.
a permission level of `Developer` or higher is required in order to manage merge requests.
![Bulk editing](img/bulk-editing.png)
To bulk update group issue or merge request milestones: To bulk update group issues, merge requests, or epics:
1. Navigate to the issues or merge requests list. 1. Navigate to the issues, merge requests, or epics list.
1. Click the **Edit issues** or **Edit merge requests** button. 1. Click **Edit issues**, **Edit merge requests**, or **Edit epics**.
- This will open a sidebar on the right-hand side of your screen where an editable field - This will open a sidebar on the right-hand side where editable fields
for milestones will be displayed. for milestones and labels will be displayed.
- Checkboxes will also appear beside each issue or merge request. - Checkboxes will also appear beside each issue, merge request, or epic.
1. Check the checkbox beside each issue to be edited. 1. Check the checkbox beside each issue, merge request, or epic to be edited.
1. Select the desired milestone from the sidebar. 1. Select the desired new values from the sidebar.
1. Click **Update all**. 1. Click **Update all**.
...@@ -97,6 +97,22 @@ have a [start or due date](#start-date-and-due-date), then you can see a ...@@ -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. 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 ## Deleting an epic
NOTE: **Note:** NOTE: **Note:**
......
...@@ -31,7 +31,159 @@ describe Issuable::BulkUpdateService do ...@@ -31,7 +31,159 @@ describe Issuable::BulkUpdateService do
end end
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 describe 'close issues' do
let(:issues) { create_list(:issue, 2, project: project) } let(:issues) { create_list(:issue, 2, project: project) }
...@@ -178,159 +330,11 @@ describe Issuable::BulkUpdateService do ...@@ -178,159 +330,11 @@ describe Issuable::BulkUpdateService do
end end
describe 'updating labels' do 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(:bug) { create(:label, project: project) }
let(:regression) { create(:label, project: project) } let(:regression) { create(:label, project: project) }
let(:merge_requests) { create(:label, project: project) } let(:merge_requests) { create(:label, project: project) }
let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } it_behaves_like 'updating labels'
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 end
describe 'subscribe to issues' do describe 'subscribe to issues' do
...@@ -360,7 +364,7 @@ describe Issuable::BulkUpdateService do ...@@ -360,7 +364,7 @@ describe Issuable::BulkUpdateService do
end end
end end
context 'with group issuables ' do context 'with issuables at a group level' do
let(:group) { create(:group) } let(:group) { create(:group) }
describe 'updating milestones' do describe 'updating milestones' do
...@@ -387,5 +391,18 @@ describe Issuable::BulkUpdateService do ...@@ -387,5 +391,18 @@ describe Issuable::BulkUpdateService do
it_behaves_like 'updates milestones' it_behaves_like 'updates milestones'
end end
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
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment