Add a close issue slack slash command

Adds a slash command in slach for closing issues. See
https://docs.gitlab.com/ee/integration/slash_commands.html
for documentation on the wider feature set.
This commit is contained in:
Sarah Yasonik 2019-08-29 20:27:38 +00:00 committed by Mayra Cabrera
parent 6712b13393
commit 94720682a1
9 changed files with 218 additions and 9 deletions

View file

@ -0,0 +1,5 @@
---
title: Add a close issue slack slash command
merge_request: 32150
author:
type: added

View file

@ -15,6 +15,7 @@ Taking the trigger term as `project-name`, the commands are:
| `/project-name help` | Shows all available slash commands |
| `/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 close <id>` | Closes 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 |

View file

@ -9,6 +9,7 @@ module Gitlab
Gitlab::SlashCommands::IssueNew,
Gitlab::SlashCommands::IssueSearch,
Gitlab::SlashCommands::IssueMove,
Gitlab::SlashCommands::IssueClose,
Gitlab::SlashCommands::Deploy,
Gitlab::SlashCommands::Run
]

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Gitlab
module SlashCommands
class IssueClose < IssueCommand
def self.match(text)
/\Aissue\s+close\s+#{Issue.reference_prefix}?(?<iid>\d+)/.match(text)
end
def self.help_message
"issue close <id>"
end
def self.allowed?(project, user)
can?(user, :update_issue, project)
end
def execute(match)
issue = find_by_iid(match[:iid])
return not_found unless issue
return presenter(issue).already_closed if issue.closed?
close_issue(issue: issue)
presenter(issue).present
end
private
def close_issue(issue:)
Issues::CloseService.new(project, current_user).execute(issue)
end
def presenter(issue)
Gitlab::SlashCommands::Presenters::IssueClose.new(issue)
end
def not_found
Gitlab::SlashCommands::Presenters::Access.new.not_found
end
end
end
end

View file

@ -40,6 +40,14 @@ module Gitlab
]
end
def project_link
"[#{project.full_name}](#{project.web_url})"
end
def author_profile_link
"[#{author.to_reference}](#{url_for(author)})"
end
private
attr_reader :resource

View file

@ -0,0 +1,51 @@
# frozen_string_literal: true
module Gitlab
module SlashCommands
module Presenters
class IssueClose < Presenters::Base
include Presenters::IssueBase
def present
if @resource.confidential?
ephemeral_response(close_issue)
else
in_channel_response(close_issue)
end
end
def already_closed
ephemeral_response(text: "Issue #{@resource.to_reference} is already closed.")
end
private
def close_issue
{
attachments: [
{
title: "#{@resource.title} · #{@resource.to_reference}",
title_link: resource_url,
author_name: author.name,
author_icon: author.avatar_url,
fallback: "Closed issue #{@resource.to_reference}: #{@resource.title}",
pretext: pretext,
color: color(@resource),
fields: fields,
mrkdwn_in: [
:title,
:pretext,
:fields
]
}
]
}
end
def pretext
"I closed an issue on #{author_profile_link}'s behalf: *#{@resource.to_reference}* in #{project_link}"
end
end
end
end
end

View file

@ -36,15 +36,7 @@ module Gitlab
end
def pretext
"I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
end
def project_link
"[#{project.full_name}](#{project.web_url})"
end
def author_profile_link
"[#{author.to_reference}](#{url_for(author)})"
"I created an issue on #{author_profile_link}'s behalf: *#{@resource.to_reference}* in #{project_link}"
end
end
end

View file

@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::SlashCommands::IssueClose do
describe '#execute' do
let(:issue) { create(:issue, project: project) }
let(:project) { create(:project) }
let(:user) { issue.author }
let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match("issue close #{issue.iid}") }
subject do
described_class.new(project, chat_name).execute(regex_match)
end
context 'when the user does not have permission' do
let(:chat_name) { double(:chat_name, user: create(:user)) }
it 'does not allow the user to close the issue' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to match("not found")
expect(issue.reload).to be_open
end
end
context 'the issue exists' do
let(:title) { subject[:attachments].first[:title] }
it 'closes and returns the issue' do
expect(subject[:response_type]).to be(:in_channel)
expect(issue.reload).to be_closed
expect(title).to start_with(issue.title)
end
context 'when its reference is given' do
let(:regex_match) { described_class.match("issue close #{issue.to_reference}") }
it 'closes and returns the issue' do
expect(subject[:response_type]).to be(:in_channel)
expect(issue.reload).to be_closed
expect(title).to start_with(issue.title)
end
end
end
context 'the issue does not exist' do
let(:regex_match) { described_class.match("issue close 2343242") }
it "returns not found" do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to match("not found")
end
end
context 'when the issue is already closed' do
let(:issue) { create(:issue, :closed, project: project) }
it 'shows the issue' do
expect(subject[:response_type]).to be(:ephemeral)
expect(issue.reload).to be_closed
expect(subject[:text]).to match("already closed")
end
end
end
describe '.match' do
it 'matches the iid' do
match = described_class.match("issue close 123")
expect(match[:iid]).to eq("123")
end
it 'accepts a reference' do
match = described_class.match("issue close #{Issue.reference_prefix}123")
expect(match[:iid]).to eq("123")
end
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::SlashCommands::Presenters::IssueClose do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
let(:attachment) { subject[:attachments].first }
subject { described_class.new(issue).present }
it { is_expected.to be_a(Hash) }
it 'shows the issue' do
expect(subject[:response_type]).to be(:in_channel)
expect(subject).to have_key(:attachments)
expect(attachment[:title]).to start_with(issue.title)
end
context 'confidential issue' do
let(:issue) { create(:issue, :confidential, project: project) }
it 'shows an ephemeral response' do
expect(subject[:response_type]).to be(:ephemeral)
end
end
end