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 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 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 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 search <query>` | Shows up to 5 issues matching `<query>` |
|
||||||
| `/project-name issue move <id> to <project>` | Moves issue ID `<id>` to `<project>` |
|
| `/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 |
|
| `/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::IssueNew,
|
||||||
Gitlab::SlashCommands::IssueSearch,
|
Gitlab::SlashCommands::IssueSearch,
|
||||||
Gitlab::SlashCommands::IssueMove,
|
Gitlab::SlashCommands::IssueMove,
|
||||||
|
Gitlab::SlashCommands::IssueClose,
|
||||||
Gitlab::SlashCommands::Deploy,
|
Gitlab::SlashCommands::Deploy,
|
||||||
Gitlab::SlashCommands::Run
|
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
|
end
|
||||||
|
|
||||||
|
def project_link
|
||||||
|
"[#{project.full_name}](#{project.web_url})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def author_profile_link
|
||||||
|
"[#{author.to_reference}](#{url_for(author)})"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :resource
|
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
|
end
|
||||||
|
|
||||||
def pretext
|
def pretext
|
||||||
"I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
|
"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)})"
|
|
||||||
end
|
end
|
||||||
end
|
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