diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 2f47cf5f95e81986ee1f032c7eda844f5d24b174..a5da9d465d4855fb67603436dc86d661de843775 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -52,6 +52,7 @@ discussions, and descriptions: | `/target_branch ` | Set target branch | | ✓ | | `/wip` | Toggle the Work In Progress status | | ✓ | | `/merge` | Merge (when pipeline succeeds) | | ✓ | +| `/relate #issue [#issue...]`| Mark issues as related | ✓ | | ## Quick actions for commit messages diff --git a/ee/app/services/ee/issues/update_service.rb b/ee/app/services/ee/issues/update_service.rb index b83a90737b6f50d8f15f5a696b61493d876a63cb..a6b70180125b6ce891282876e0247f9653f4f063 100644 --- a/ee/app/services/ee/issues/update_service.rb +++ b/ee/app/services/ee/issues/update_service.rb @@ -8,6 +8,7 @@ module UpdateService override :execute def execute(issue) handle_epic(issue) + handle_relate(issue) result = super if issue.previous_changes.include?(:milestone_id) && issue.epic @@ -34,6 +35,18 @@ def handle_epic(issue) EpicIssues::DestroyService.new(link, current_user).execute end end + + def handle_relate(issue) + return unless params.key?(:related_issues) + + relate_param = params.delete(:related_issues) + + if relate_param + relate_param.each do |issuable| + IssueLinks::CreateService.new(issuable, current_user, { target_issue: issue }).execute + end + end + end end end end diff --git a/ee/app/services/ee/quick_actions/interpret_service.rb b/ee/app/services/ee/quick_actions/interpret_service.rb index 9e3b117a1a51a4e8e32bdcb9551d7a686c027809..ee84f33d40bcd254637a4b0fb51815a910ff0aba 100644 --- a/ee/app/services/ee/quick_actions/interpret_service.rb +++ b/ee/app/services/ee/quick_actions/interpret_service.rb @@ -77,6 +77,20 @@ def extract_epic(params) extract_references(params, :epic).first end + + desc 'Mark this issue as related to another issue' + explanation do |related_reference| + "Marks this issue related to #{related_reference}." + end + params '#issue' + condition do + issuable.is_a?(Issue) && + issuable.persisted? && + current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + end + command :relate do |related_param| + @updates[:related_issues] = extract_references(related_param, :issue) + end end end end diff --git a/ee/changelogs/unreleased-ee/6249-related-quick-action.yml b/ee/changelogs/unreleased-ee/6249-related-quick-action.yml new file mode 100644 index 0000000000000000000000000000000000000000..ac8aa3adc1e65b29024f0ce247ecb323afe67d7d --- /dev/null +++ b/ee/changelogs/unreleased-ee/6249-related-quick-action.yml @@ -0,0 +1,5 @@ +--- +title: Related quick action added +merge_request: 8002 +author: William George +type: added diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb index ccca4c1f6c8bb08d6d86d68526037beacb168f7e..1990821af8d0773a1ea44d01b249e9d4804dc76e 100644 --- a/spec/features/issues/user_uses_quick_actions_spec.rb +++ b/spec/features/issues/user_uses_quick_actions_spec.rb @@ -227,6 +227,36 @@ end end + describe 'mark issue as related' do + let(:issue) { create(:issue, project: project) } + let(:original_issue) { create(:issue, project: project) } + + context 'when the current user can update issues' do + it 'does not create a note, and marks the issue as releated' do + add_note("/relate ##{original_issue.to_reference}") + + expect(page).not_to have_content "/relate #{original_issue.to_reference}" + expect(page).to have_content 'Commands applied' + end + end + + context 'when the current user cannot update the issue' do + let(:guest) { create(:user) } + before do + project.add_guest(guest) + gitlab_sign_out + sign_in(guest) + visit project_issue_path(project, issue) + end + + it 'does not create a note, and does not mark the issue as a duplicate' do + add_note("/relate ##{original_issue.to_reference}") + + expect(page).not_to have_content 'Commands applied' + end + end + end + describe 'make issue confidential' do let(:issue) { create(:issue, project: project) } let(:original_issue) { create(:issue, project: project) } diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 116866605fc26d67a31124a53340c15df5978ff3..bb4c2baf06709434fc940a70eee3bf5de6ca9630 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -350,6 +350,15 @@ end end + shared_examples 'related command' do + it 'fetches issue and populates related_issues if content contains /relate related_issues' do + issue_related # populate the issue + _, updates = service.execute(content, issuable) + + expect(updates[:related_issues]).to match_array([issue_related]) + end + end + shared_examples 'copy_metadata command' do it 'fetches issue or merge request and copies labels and milestone if content contains /copy_metadata reference' do source_issuable # populate the issue @@ -1029,6 +1038,56 @@ end end + context '/relate command' do + it_behaves_like 'related command' do + let(:issue_related) { create(:issue, project: project) } + let(:content) { "/relate #{issue_related.to_reference}" } + let(:issuable) { issue } + end + + it_behaves_like 'related command' do + let(:issue_related) { create(:issue, project: project) } + let(:issue_related_another) { create(:issue, project: project) } + let(:content) { "/relate #{issue_related.to_reference} #{issue_related_another.to_reference}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/relate' } + let(:issuable) { issue } + end + + context 'cross project references' do + it_behaves_like 'related command' do + let(:other_project) { create(:project, :public) } + let(:issue_related) { create(:issue, project: other_project) } + let(:content) { "/relate #{issue_related.to_reference(project)}" } + let(:issuable) { issue } + end + + it_behaves_like 'related command' do + let(:other_project) { create(:project, :public) } + let(:issue_related) { create(:issue, project: other_project) } + let(:issue_related_another) { create(:issue, project: other_project) } + let(:content) { "/relate #{issue_related.to_reference(project)} #{issue_related_another.to_reference(project)}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { "/relate imaginary#1234" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:other_project) { create(:project, :private) } + let(:issue_related) { create(:issue, project: other_project) } + + let(:content) { "/relate #{issue_related.to_reference(project)}" } + let(:issuable) { issue } + end + end + end + context 'when current_user cannot :admin_issue' do let(:visitor) { create(:user) } let(:issue) { create(:issue, project: project, author: visitor) } @@ -1089,6 +1148,12 @@ let(:issuable) { issue } end + it_behaves_like 'empty command' do + let(:issue_related) { create(:issue, project: project) } + let(:content) { '/relate #{issue_related.to_reference}' } + let(:issuable) { issue } + end + it_behaves_like 'empty command' do let(:content) { '/lock' } let(:issuable) { issue }