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:
parent
6712b13393
commit
94720682a1
9 changed files with 218 additions and 9 deletions
5
changelogs/unreleased/ce-slack-close-command.yml
Normal file
5
changelogs/unreleased/ce-slack-close-command.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add a close issue slack slash command
|
||||
merge_request: 32150
|
||||
author:
|
||||
type: added
|
|
@ -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 |
|
||||
|
|
|
@ -9,6 +9,7 @@ module Gitlab
|
|||
Gitlab::SlashCommands::IssueNew,
|
||||
Gitlab::SlashCommands::IssueSearch,
|
||||
Gitlab::SlashCommands::IssueMove,
|
||||
Gitlab::SlashCommands::IssueClose,
|
||||
Gitlab::SlashCommands::Deploy,
|
||||
Gitlab::SlashCommands::Run
|
||||
]
|
||||
|
|
44
lib/gitlab/slash_commands/issue_close.rb
Normal file
44
lib/gitlab/slash_commands/issue_close.rb
Normal 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
|
|
@ -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
|
||||
|
|
51
lib/gitlab/slash_commands/presenters/issue_close.rb
Normal file
51
lib/gitlab/slash_commands/presenters/issue_close.rb
Normal 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
|
|
@ -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
|
||||
|
|
80
spec/lib/gitlab/slash_commands/issue_close_spec.rb
Normal file
80
spec/lib/gitlab/slash_commands/issue_close_spec.rb
Normal 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
|
|
@ -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
|
Loading…
Reference in a new issue