From 94720682a1314f769f37835d8e4e3df21650e77c Mon Sep 17 00:00:00 2001 From: Sarah Yasonik Date: Thu, 29 Aug 2019 20:27:38 +0000 Subject: 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. --- changelogs/unreleased/ce-slack-close-command.yml | 5 ++ doc/integration/slash_commands.md | 1 + lib/gitlab/slash_commands/command.rb | 1 + lib/gitlab/slash_commands/issue_close.rb | 44 ++++++++++++ lib/gitlab/slash_commands/presenters/issue_base.rb | 8 +++ .../slash_commands/presenters/issue_close.rb | 51 ++++++++++++++ lib/gitlab/slash_commands/presenters/issue_new.rb | 10 +-- spec/lib/gitlab/slash_commands/issue_close_spec.rb | 80 ++++++++++++++++++++++ .../slash_commands/presenters/issue_close_spec.rb | 27 ++++++++ 9 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 changelogs/unreleased/ce-slack-close-command.yml create mode 100644 lib/gitlab/slash_commands/issue_close.rb create mode 100644 lib/gitlab/slash_commands/presenters/issue_close.rb create mode 100644 spec/lib/gitlab/slash_commands/issue_close_spec.rb create mode 100644 spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb diff --git a/changelogs/unreleased/ce-slack-close-command.yml b/changelogs/unreleased/ce-slack-close-command.yml new file mode 100644 index 00000000000..c20e623b117 --- /dev/null +++ b/changelogs/unreleased/ce-slack-close-command.yml @@ -0,0 +1,5 @@ +--- +title: Add a close issue slack slash command +merge_request: 32150 +author: +type: added diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index 71ea2e25533..86a66dc4569 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -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 <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 | diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb index 7c963fcf38a..905e0ec5cc1 100644 --- a/lib/gitlab/slash_commands/command.rb +++ b/lib/gitlab/slash_commands/command.rb @@ -9,6 +9,7 @@ module Gitlab Gitlab::SlashCommands::IssueNew, Gitlab::SlashCommands::IssueSearch, Gitlab::SlashCommands::IssueMove, + Gitlab::SlashCommands::IssueClose, Gitlab::SlashCommands::Deploy, Gitlab::SlashCommands::Run ] diff --git a/lib/gitlab/slash_commands/issue_close.rb b/lib/gitlab/slash_commands/issue_close.rb new file mode 100644 index 00000000000..5fcc86e91c4 --- /dev/null +++ b/lib/gitlab/slash_commands/issue_close.rb @@ -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 diff --git a/lib/gitlab/slash_commands/presenters/issue_base.rb b/lib/gitlab/slash_commands/presenters/issue_base.rb index b6db103b82b..08cb82274fd 100644 --- a/lib/gitlab/slash_commands/presenters/issue_base.rb +++ b/lib/gitlab/slash_commands/presenters/issue_base.rb @@ -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 diff --git a/lib/gitlab/slash_commands/presenters/issue_close.rb b/lib/gitlab/slash_commands/presenters/issue_close.rb new file mode 100644 index 00000000000..b3f24f4296a --- /dev/null +++ b/lib/gitlab/slash_commands/presenters/issue_close.rb @@ -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 diff --git a/lib/gitlab/slash_commands/presenters/issue_new.rb b/lib/gitlab/slash_commands/presenters/issue_new.rb index ac78745ae70..1424a4ac381 100644 --- a/lib/gitlab/slash_commands/presenters/issue_new.rb +++ b/lib/gitlab/slash_commands/presenters/issue_new.rb @@ -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 diff --git a/spec/lib/gitlab/slash_commands/issue_close_spec.rb b/spec/lib/gitlab/slash_commands/issue_close_spec.rb new file mode 100644 index 00000000000..c0760ce0ba6 --- /dev/null +++ b/spec/lib/gitlab/slash_commands/issue_close_spec.rb @@ -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 diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb new file mode 100644 index 00000000000..adc13b4ee56 --- /dev/null +++ b/spec/lib/gitlab/slash_commands/presenters/issue_close_spec.rb @@ -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 -- cgit v1.2.1