Commit f9a97196 authored by Robert Speicher's avatar Robert Speicher

Merge branch '196795-iteration-issue-quick_actions' into 'master'

Add quick actions for iterations in issues

Closes #196795

See merge request gitlab-org/gitlab!32904
parents 5ec9feb7 84675019
......@@ -40,6 +40,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/duplicate <#issue>` | ✓ | | | Mark this issue as a duplicate of another issue and mark them as related. **(STARTER)** |
| `/epic <epic>` | ✓ | | | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. **(PREMIUM)** |
| `/estimate <<W>w <DD>d <hh>h <mm>m>` | ✓ | ✓ | | Set time estimate. For example, `/estimate 1w 3d 2h 14m`. |
| `/iteration *iteration:iteration` | ✓ | | | Set iteration ([Introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)) **(STARTER)** |
| `/label ~label1 ~label2` | ✓ | ✓ | ✓ | Add one or more labels. Label names can also start without a tilde (`~`), but mixed syntax is not supported. |
| `/lock` | ✓ | ✓ | | Lock the thread. |
| `/merge` | | ✓ | | Merge changes. Depending on the project setting, this may be [when the pipeline succeeds](merge_requests/merge_when_pipeline_succeeds.md), adding to a [Merge Train](../../ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md), etc. |
......@@ -54,6 +55,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/remove_due_date` | ✓ | | | Remove due date. |
| `/remove_epic` | ✓ | | | Remove from epic. **(PREMIUM)** |
| `/remove_estimate` | ✓ | ✓ | | Remove time estimate. |
| `/remove_iteration` | ✓ | | | Remove iteration ([Introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)) **(STARTER)** |
| `/remove_milestone` | ✓ | ✓ | | Remove milestone. |
| `/remove_parent_epic` | | | ✓ | Remove parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab/-/issues/10556)). **(ULTIMATE)** |
| `/remove_time_spent` | ✓ | ✓ | | Remove time spent. |
......
---
title: Add quick actions for iterations in issues
merge_request: 32904
author:
type: added
......@@ -83,11 +83,57 @@ module EE
end
end
desc _('Set iteration')
explanation do |iteration|
_("Sets the iteration to %{iteration_reference}.") % { iteration_reference: iteration.to_reference } if iteration
end
execution_message do |iteration|
_("Set the iteration to %{iteration_reference}.") % { iteration_reference: iteration.to_reference } if iteration
end
params '*iteration:"iteration"'
types Issue
condition do
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) &&
quick_action_target.project.group&.feature_available?(:iterations) &&
find_iterations(project, state: 'active').any?
end
parse_params do |iteration_param|
extract_references(iteration_param, :iteration).first ||
find_iterations(project, title: iteration_param.strip).first
end
command :iteration do |iteration|
@updates[:iteration] = iteration if iteration
end
desc _('Remove iteration')
explanation do
_("Removes %{iteration_reference} iteration.") % { iteration_reference: quick_action_target.iteration.to_reference(format: :name) }
end
execution_message do
_("Removed %{iteration_reference} iteration.") % { iteration_reference: quick_action_target.iteration.to_reference(format: :name) }
end
types Issue
condition do
quick_action_target.persisted? &&
quick_action_target.sprint_id? &&
quick_action_target.project.group&.feature_available?(:iterations) &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
end
command :remove_iteration do
@updates[:iteration] = nil
end
def extract_epic(params)
return if params.nil?
extract_references(params, :epic).first
end
def find_iterations(project, params = {})
group_ids = project.group.self_and_ancestors.select(:id) if project.group
::IterationsFinder.new(params.merge(project_ids: [project.id], group_ids: group_ids)).execute
end
end
end
end
......
......@@ -201,7 +201,6 @@ describe Banzai::Filter::IterationReferenceFilter do
it 'points to referenced project iteration page' do
expect(result.css('a').first.attr('href'))
.to eq(urls.project_iteration_url(another_project, iteration))
.project_iteration_url(another_project, iteration)
end
it 'link has valid text' do
......
......@@ -7,9 +7,9 @@ describe QuickActions::InterpretService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, :public, group: group) }
let(:issue) { create(:issue, project: project) }
let_it_be_with_refind(:group) { create(:group) }
let_it_be_with_refind(:project) { create(:project, :repository, :public, group: group) }
let_it_be_with_reload(:issue) { create(:issue, project: project) }
let(:service) { described_class.new(project, current_user) }
before do
......@@ -210,6 +210,110 @@ describe QuickActions::InterpretService do
end
end
context 'iteration command' do
let_it_be(:iteration) { create(:iteration, group: group) }
let(:content) { "/iteration #{iteration.to_reference(project)}" }
context 'when iterations are enabled' do
before do
stub_licensed_features(iterations: true)
end
context 'when iteration exists' do
it 'assigns an iteration to an issue' do
_, updates, message = service.execute(content, issue)
expect(updates).to eq(iteration: iteration)
expect(message).to eq("Set the iteration to #{iteration.to_reference}.")
end
context 'when the user does not have enough permissions' do
before do
allow(current_user).to receive(:can?).with(:use_quick_actions).and_return(true)
allow(current_user).to receive(:can?).with(:admin_issue, project).and_return(false)
end
it 'returns empty message' do
_, updates, message = service.execute(content, issue)
expect(updates).to be_empty
expect(message).to be_empty
end
end
end
context 'when iteration does not exist' do
let(:content) { "/iteration none" }
it 'returns empty message' do
_, updates, message = service.execute(content, issue)
expect(updates).to be_empty
expect(message).to be_empty
end
end
end
context 'when iterations are disabled' do
before do
stub_licensed_features(iterations: false)
end
it 'does not recognize /iteration' do
_, updates = service.execute(content, issue)
expect(updates).to be_empty
end
end
end
context 'remove_iteration command' do
let_it_be(:iteration) { create(:iteration, group: group) }
let(:content) { '/remove_iteration' }
context 'when iterations are enabled' do
before do
stub_licensed_features(iterations: true)
issue.update!(iteration: iteration)
end
it 'removes an assigned iteration from an issue' do
_, updates, message = service.execute(content, issue)
expect(updates).to eq(iteration: nil)
expect(message).to eq("Removed #{iteration.to_reference} iteration.")
end
context 'when the user does not have enough permissions' do
before do
allow(current_user).to receive(:can?).with(:use_quick_actions).and_return(true)
allow(current_user).to receive(:can?).with(:admin_issue, project).and_return(false)
end
it 'returns empty message' do
_, updates, message = service.execute(content, issue)
expect(updates).to be_empty
expect(message).to be_empty
end
end
end
context 'when iterations are disabled' do
before do
stub_licensed_features(iterations: false)
end
it 'does not recognize /remove_iteration' do
_, updates = service.execute(content, issue)
expect(updates).to be_empty
end
end
end
context 'epic command' do
let(:epic) { create(:epic, group: group) }
let(:content) { "/epic #{epic.to_reference(project)}" }
......
......@@ -17930,6 +17930,9 @@ msgstr ""
msgid "Remove group"
msgstr ""
msgid "Remove iteration"
msgstr ""
msgid "Remove license"
msgstr ""
......@@ -17975,6 +17978,9 @@ msgstr ""
msgid "Removed %{epic_ref} from child epics."
msgstr ""
msgid "Removed %{iteration_reference} iteration."
msgstr ""
msgid "Removed %{label_references} %{label_text}."
msgstr ""
......@@ -18014,6 +18020,9 @@ msgstr ""
msgid "Removes %{epic_ref} from child epics."
msgstr ""
msgid "Removes %{iteration_reference} iteration."
msgstr ""
msgid "Removes %{label_references} %{label_text}."
msgstr ""
......@@ -19614,6 +19623,9 @@ msgstr ""
msgid "Set instance-wide template repository"
msgstr ""
msgid "Set iteration"
msgstr ""
msgid "Set max session time for web terminal."
msgstr ""
......@@ -19650,6 +19662,9 @@ msgstr ""
msgid "Set the duration for which the jobs will be considered as old and expired. Once that time passes, the jobs will be archived and no longer able to be retried. Make it empty to never expire jobs. It has to be no less than 1 day, for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>."
msgstr ""
msgid "Set the iteration to %{iteration_reference}."
msgstr ""
msgid "Set the maximum file size for each job's artifacts"
msgstr ""
......@@ -19734,6 +19749,9 @@ msgstr ""
msgid "Sets the due date to %{due_date}."
msgstr ""
msgid "Sets the iteration to %{iteration_reference}."
msgstr ""
msgid "Sets the milestone to %{milestone_reference}."
msgstr ""
......
# frozen_string_literal: true
TestProf::LetItBe.configure do |config|
config.alias_to :let_it_be_with_refind, refind: true
end
TestProf::LetItBe.configure do |config|
config.alias_to :let_it_be_with_reload, reload: true
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