Merge branch 'adamco/gitlab-ce-move-issue-command' into 'master'
Add slash command for moving an issue See merge request gitlab-org/gitlab-ce!17691
This commit is contained in:
commit
81d370d419
8 changed files with 254 additions and 1 deletions
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add slash command for moving issues
|
||||
merge_request:
|
||||
author: Adam Pahlevi
|
||||
type: added
|
|
@ -15,9 +15,10 @@ Taking the trigger term as `project-name`, the commands are:
|
|||
| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
|
||||
| `/project-name issue show <id>` | Shows the issue with id `<id>` |
|
||||
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
|
||||
| `/project-name issue move <id> to <project>` | Moves issue ID `<id>` to `<project>` |
|
||||
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
|
||||
|
||||
Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
|
||||
Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
|
||||
your GitLab.com projects, you need to [add the `gitlab` keyword at the beginning of the command](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html#usage).
|
||||
|
||||
## Issue commands
|
||||
|
|
|
@ -5,6 +5,7 @@ module Gitlab
|
|||
Gitlab::SlashCommands::IssueShow,
|
||||
Gitlab::SlashCommands::IssueNew,
|
||||
Gitlab::SlashCommands::IssueSearch,
|
||||
Gitlab::SlashCommands::IssueMove,
|
||||
Gitlab::SlashCommands::Deploy
|
||||
].freeze
|
||||
|
||||
|
|
45
lib/gitlab/slash_commands/issue_move.rb
Normal file
45
lib/gitlab/slash_commands/issue_move.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
module Gitlab
|
||||
module SlashCommands
|
||||
class IssueMove < IssueCommand
|
||||
def self.match(text)
|
||||
%r{
|
||||
\A # the beginning of a string
|
||||
issue\s+move\s+ # the command
|
||||
\#?(?<iid>\d+)\s+ # the issue id, may preceded by hash sign
|
||||
(to\s+)? # aid the command to be much more human-ly
|
||||
(?<project_path>[^\s]+) # named group for id of dest. project
|
||||
}x.match(text)
|
||||
end
|
||||
|
||||
def self.help_message
|
||||
'issue move <issue_id> (to)? <project_path>'
|
||||
end
|
||||
|
||||
def self.allowed?(project, user)
|
||||
can?(user, :admin_issue, project)
|
||||
end
|
||||
|
||||
def execute(match)
|
||||
old_issue = find_by_iid(match[:iid])
|
||||
target_project = Project.find_by_full_path(match[:project_path])
|
||||
|
||||
unless current_user.can?(:read_project, target_project) && old_issue
|
||||
return Gitlab::SlashCommands::Presenters::Access.new.not_found
|
||||
end
|
||||
|
||||
new_issue = Issues::MoveService.new(project, current_user)
|
||||
.execute(old_issue, target_project)
|
||||
|
||||
presenter(new_issue).present(old_issue)
|
||||
rescue Issues::MoveService::MoveError => e
|
||||
presenter(old_issue).display_move_error(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def presenter(issue)
|
||||
Gitlab::SlashCommands::Presenters::IssueMove.new(issue)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
53
lib/gitlab/slash_commands/presenters/issue_move.rb
Normal file
53
lib/gitlab/slash_commands/presenters/issue_move.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
# coding: utf-8
|
||||
module Gitlab
|
||||
module SlashCommands
|
||||
module Presenters
|
||||
class IssueMove < Presenters::Base
|
||||
include Presenters::IssueBase
|
||||
|
||||
def present(old_issue)
|
||||
in_channel_response(moved_issue(old_issue))
|
||||
end
|
||||
|
||||
def display_move_error(error)
|
||||
message = header_with_list("The action was not successful, because:", [error])
|
||||
|
||||
ephemeral_response(text: message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def moved_issue(old_issue)
|
||||
{
|
||||
attachments: [
|
||||
{
|
||||
title: "#{@resource.title} · #{@resource.to_reference}",
|
||||
title_link: resource_url,
|
||||
author_name: author.name,
|
||||
author_icon: author.avatar_url,
|
||||
fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
|
||||
pretext: pretext(old_issue),
|
||||
color: color(@resource),
|
||||
fields: fields,
|
||||
mrkdwn_in: [
|
||||
:title,
|
||||
:pretext,
|
||||
:text,
|
||||
:fields
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
def pretext(old_issue)
|
||||
"Moved issue *#{issue_link(old_issue)}* to *#{issue_link(@resource)}*"
|
||||
end
|
||||
|
||||
def issue_link(issue)
|
||||
"[#{issue.to_reference}](#{project_issue_url(issue.project, issue)})"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -108,5 +108,10 @@ describe Gitlab::SlashCommands::Command do
|
|||
|
||||
it { is_expected.to eq(Gitlab::SlashCommands::IssueSearch) }
|
||||
end
|
||||
|
||||
context 'IssueMove is triggered' do
|
||||
let(:params) { { text: 'issue move #78291 to gitlab/gitlab-ci' } }
|
||||
it { is_expected.to eq(Gitlab::SlashCommands::IssueMove) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
117
spec/lib/gitlab/slash_commands/issue_move_spec.rb
Normal file
117
spec/lib/gitlab/slash_commands/issue_move_spec.rb
Normal file
|
@ -0,0 +1,117 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::SlashCommands::IssueMove, service: true do
|
||||
describe '#match' do
|
||||
shared_examples_for 'move command' do |text_command|
|
||||
it 'can be parsed to extract the needed fields' do
|
||||
match_data = described_class.match(text_command)
|
||||
|
||||
expect(match_data['iid']).to eq('123456')
|
||||
expect(match_data['project_path']).to eq('gitlab/gitlab-ci')
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'move command', 'issue move #123456 to gitlab/gitlab-ci'
|
||||
it_behaves_like 'move command', 'issue move #123456 gitlab/gitlab-ci'
|
||||
it_behaves_like 'move command', 'issue move #123456 gitlab/gitlab-ci '
|
||||
it_behaves_like 'move command', 'issue move 123456 to gitlab/gitlab-ci'
|
||||
it_behaves_like 'move command', 'issue move 123456 gitlab/gitlab-ci'
|
||||
it_behaves_like 'move command', 'issue move 123456 gitlab/gitlab-ci '
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
set(:user) { create(:user) }
|
||||
set(:issue) { create(:issue) }
|
||||
set(:chat_name) { create(:chat_name, user: user) }
|
||||
set(:project) { issue.project }
|
||||
set(:other_project) { create(:project, namespace: project.namespace) }
|
||||
|
||||
before do
|
||||
[project, other_project].each { |prj| prj.add_master(user) }
|
||||
end
|
||||
|
||||
subject { described_class.new(project, chat_name) }
|
||||
|
||||
def process_message(message)
|
||||
subject.execute(described_class.match(message))
|
||||
end
|
||||
|
||||
context 'when the user can move the issue' do
|
||||
context 'when the move fails' do
|
||||
it 'returns the error message' do
|
||||
message = "issue move #{issue.iid} #{project.full_path}"
|
||||
|
||||
expect(process_message(message)).to include(response_type: :ephemeral,
|
||||
text: a_string_matching('Cannot move issue'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the move succeeds' do
|
||||
let(:message) { "issue move #{issue.iid} #{other_project.full_path}" }
|
||||
|
||||
it 'moves the issue to the new destination' do
|
||||
expect { process_message(message) }.to change { Issue.count }.by(1)
|
||||
|
||||
new_issue = issue.reload.moved_to
|
||||
|
||||
expect(new_issue.state).to eq('opened')
|
||||
expect(new_issue.project_id).to eq(other_project.id)
|
||||
expect(new_issue.author_id).to eq(issue.author_id)
|
||||
|
||||
expect(issue.state).to eq('closed')
|
||||
expect(issue.project_id).to eq(project.id)
|
||||
end
|
||||
|
||||
it 'returns the new issue' do
|
||||
expect(process_message(message))
|
||||
.to include(response_type: :in_channel,
|
||||
attachments: [a_hash_including(title_link: a_string_including(other_project.full_path))])
|
||||
end
|
||||
|
||||
it 'mentions the old issue' do
|
||||
expect(process_message(message))
|
||||
.to include(attachments: [a_hash_including(pretext: a_string_including(project.full_path))])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the issue does not exist' do
|
||||
it 'returns not found' do
|
||||
message = "issue move #{issue.iid.succ} #{other_project.full_path}"
|
||||
|
||||
expect(process_message(message)).to include(response_type: :ephemeral,
|
||||
text: a_string_matching('not found'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the target project does not exist' do
|
||||
it 'returns not found' do
|
||||
message = "issue move #{issue.iid} #{other_project.full_path}/foo"
|
||||
|
||||
expect(process_message(message)).to include(response_type: :ephemeral,
|
||||
text: a_string_matching('not found'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user cannot see the target project' do
|
||||
it 'returns not found' do
|
||||
message = "issue move #{issue.iid} #{other_project.full_path}"
|
||||
other_project.team.truncate
|
||||
|
||||
expect(process_message(message)).to include(response_type: :ephemeral,
|
||||
text: a_string_matching('not found'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user does not have the required permissions on the target project' do
|
||||
it 'returns the error message' do
|
||||
message = "issue move #{issue.iid} #{other_project.full_path}"
|
||||
other_project.team.truncate
|
||||
other_project.team.add_guest(user)
|
||||
|
||||
expect(process_message(message)).to include(response_type: :ephemeral,
|
||||
text: a_string_matching('Cannot move issue'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
26
spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb
Normal file
26
spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::SlashCommands::Presenters::IssueMove do
|
||||
set(:admin) { create(:admin) }
|
||||
set(:project) { create(:project) }
|
||||
set(:other_project) { create(:project) }
|
||||
set(:old_issue) { create(:issue, project: project) }
|
||||
set(:new_issue) { Issues::MoveService.new(project, admin).execute(old_issue, other_project) }
|
||||
let(:attachment) { subject[:attachments].first }
|
||||
|
||||
subject { described_class.new(new_issue).present(old_issue) }
|
||||
|
||||
it { is_expected.to be_a(Hash) }
|
||||
|
||||
it 'shows the new issue' do
|
||||
expect(subject[:response_type]).to be(:in_channel)
|
||||
expect(subject).to have_key(:attachments)
|
||||
expect(attachment[:title]).to start_with(new_issue.title)
|
||||
expect(attachment[:title_link]).to include(other_project.full_path)
|
||||
end
|
||||
|
||||
it 'mentions the old issue and the new issue in the pretext' do
|
||||
expect(attachment[:pretext]).to include(project.full_path)
|
||||
expect(attachment[:pretext]).to include(other_project.full_path)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue