Commit c302633e authored by Justin Ho Tuan Duong's avatar Justin Ho Tuan Duong Committed by Igor Drozdov

Update Jira comment to include more information

Backend

* Add the branch name to Jira comments.
* If `comment_detail` is set to `all_details`, include more information like commit SHA / MR number and full commit message as described in https://gitlab.com/gitlab-org/gitlab/-/issues/195887#comment-types-examples.
* Add some simple specs and update existing ones.

Frontend

* Expand the Vue component to include the Jira trigger fields.
* Add wrapping component `IntegrationForm`.
* Add `JiraTriggerFields` which shows/hides sections based on user selection.
* Use translations
parent cbc27027
<script>
import ActiveToggle from './active_toggle.vue';
import JiraTriggerFields from './jira_trigger_fields.vue';
export default {
name: 'IntegrationForm',
components: {
ActiveToggle,
JiraTriggerFields,
},
props: {
activeToggleProps: {
type: Object,
required: true,
},
showActive: {
type: Boolean,
required: true,
},
triggerFieldsProps: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
},
computed: {
isJira() {
return this.type === 'jira';
},
},
};
</script>
<template>
<div>
<active-toggle v-if="showActive" v-bind="activeToggleProps" />
<jira-trigger-fields v-if="isJira" v-bind="triggerFieldsProps" />
</div>
</template>
<script>
import { GlFormCheckbox, GlFormRadio } from '@gitlab/ui';
export default {
name: 'JiraTriggerFields',
components: {
GlFormCheckbox,
GlFormRadio,
},
props: {
initialTriggerCommit: {
type: Boolean,
required: true,
},
initialTriggerMergeRequest: {
type: Boolean,
required: true,
},
initialEnableComments: {
type: Boolean,
required: true,
},
initialCommentDetail: {
type: String,
required: false,
default: 'standard',
},
},
data() {
return {
triggerCommit: this.initialTriggerCommit,
triggerMergeRequest: this.initialTriggerMergeRequest,
enableComments: this.initialEnableComments,
commentDetail: this.initialCommentDetail,
};
},
};
</script>
<template>
<div class="form-group row pt-2" role="group">
<label for="service[trigger]" class="col-form-label col-sm-2 pt-0">{{ __('Trigger') }}</label>
<div class="col-sm-10">
<label class="weight-normal mb-2">
{{
s__(
'Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) will be created.',
)
}}
</label>
<input name="service[commit_events]" type="hidden" value="false" />
<gl-form-checkbox v-model="triggerCommit" name="service[commit_events]">
{{ __('Commit') }}
</gl-form-checkbox>
<input name="service[merge_requests_events]" type="hidden" value="false" />
<gl-form-checkbox v-model="triggerMergeRequest" name="service[merge_requests_events]">
{{ __('Merge request') }}
</gl-form-checkbox>
<div
v-show="triggerCommit || triggerMergeRequest"
class="mt-4"
data-testid="comment-settings"
>
<label>
{{ s__('Integrations|Comment settings:') }}
</label>
<input name="service[comment_on_event_enabled]" type="hidden" value="false" />
<gl-form-checkbox v-model="enableComments" name="service[comment_on_event_enabled]">
{{ s__('Integrations|Enable comments') }}
</gl-form-checkbox>
<div v-show="enableComments" class="mt-4" data-testid="comment-detail">
<label>
{{ s__('Integrations|Comment detail:') }}
</label>
<gl-form-radio v-model="commentDetail" value="standard" name="service[comment_detail]">
{{ s__('Integrations|Standard') }}
<template #help>
{{ s__('Integrations|Includes commit title and branch') }}
</template>
</gl-form-radio>
<gl-form-radio v-model="commentDetail" value="all_details" name="service[comment_detail]">
{{ s__('Integrations|All details') }}
<template #help>
{{
s__(
'Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs',
)
}}
</template>
</gl-form-radio>
</div>
</div>
</div>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import ActiveToggle from './components/active_toggle.vue'; import IntegrationForm from './components/integration_form.vue';
export default el => { export default el => {
if (!el) { if (!el) {
return null; return null;
} }
const { showActive: showActiveStr, activated: activatedStr } = el.dataset; function parseBooleanInData(data) {
const showActive = parseBoolean(showActiveStr); const result = {};
const activated = parseBoolean(activatedStr); Object.entries(data).forEach(([key, value]) => {
result[key] = parseBoolean(value);
if (!showActive) { });
return null; return result;
} }
const { type, commentDetail, ...booleanAttributes } = el.dataset;
const {
showActive,
activated,
commitEvents,
mergeRequestEvents,
enableComments,
} = parseBooleanInData(booleanAttributes);
return new Vue({ return new Vue({
el, el,
render(createElement) { render(createElement) {
return createElement(ActiveToggle, { return createElement(IntegrationForm, {
props: { props: {
initialActivated: activated, activeToggleProps: {
initialActivated: activated,
},
showActive,
type,
triggerFieldsProps: {
initialTriggerCommit: commitEvents,
initialTriggerMergeRequest: mergeRequestEvents,
initialEnableComments: enableComments,
initialCommentDetail: commentDetail,
},
}, },
}); });
}, },
......
...@@ -177,6 +177,7 @@ class JiraService < IssueTrackerService ...@@ -177,6 +177,7 @@ class JiraService < IssueTrackerService
noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
noteable_type = noteable_name(noteable) noteable_type = noteable_name(noteable)
entity_url = build_entity_url(noteable_type, noteable_id) entity_url = build_entity_url(noteable_type, noteable_id)
entity_meta = build_entity_meta(noteable)
data = { data = {
user: { user: {
...@@ -185,12 +186,15 @@ class JiraService < IssueTrackerService ...@@ -185,12 +186,15 @@ class JiraService < IssueTrackerService
}, },
project: { project: {
name: project.full_path, name: project.full_path,
url: resource_url(namespace_project_path(project.namespace, project)) # rubocop:disable Cop/ProjectPathHelper url: resource_url(project_path(project))
}, },
entity: { entity: {
id: entity_meta[:id],
name: noteable_type.humanize.downcase, name: noteable_type.humanize.downcase,
url: entity_url, url: entity_url,
title: noteable.title title: noteable.title,
description: entity_meta[:description],
branch: entity_meta[:branch]
} }
} }
...@@ -264,14 +268,11 @@ class JiraService < IssueTrackerService ...@@ -264,14 +268,11 @@ class JiraService < IssueTrackerService
end end
def add_comment(data, issue) def add_comment(data, issue)
user_name = data[:user][:name]
user_url = data[:user][:url]
entity_name = data[:entity][:name] entity_name = data[:entity][:name]
entity_url = data[:entity][:url] entity_url = data[:entity][:url]
entity_title = data[:entity][:title] entity_title = data[:entity][:title]
project_name = data[:project][:name]
message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'" message = comment_message(data)
link_title = "#{entity_name.capitalize} - #{entity_title}" link_title = "#{entity_name.capitalize} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title) link_props = build_remote_link_props(url: entity_url, title: link_title)
...@@ -280,6 +281,37 @@ class JiraService < IssueTrackerService ...@@ -280,6 +281,37 @@ class JiraService < IssueTrackerService
end end
end end
def comment_message(data)
user_link = build_jira_link(data[:user][:name], data[:user][:url])
entity = data[:entity]
entity_ref = all_details? ? "#{entity[:name]} #{entity[:id]}" : "a #{entity[:name]}"
entity_link = build_jira_link(entity_ref, entity[:url])
project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project))
branch =
if entity[:branch].present?
s_('JiraService| on branch %{branch_link}') % {
branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))
}
end
entity_message = entity[:description].presence if all_details?
entity_message ||= entity[:title].chomp
s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % {
user_link: user_link,
entity_link: entity_link,
project_link: project_link,
branch: branch,
entity_message: entity_message
}
end
def build_jira_link(title, url)
"[#{title}|#{url}]"
end
def has_resolution?(issue) def has_resolution?(issue)
issue.respond_to?(:resolution) && issue.resolution.present? issue.respond_to?(:resolution) && issue.resolution.present?
end end
...@@ -353,6 +385,23 @@ class JiraService < IssueTrackerService ...@@ -353,6 +385,23 @@ class JiraService < IssueTrackerService
) )
end end
def build_entity_meta(noteable)
if noteable.is_a?(Commit)
{
id: noteable.short_id,
description: noteable.safe_message,
branch: noteable.ref_names(project.repository).first
}
elsif noteable.is_a?(MergeRequest)
{
id: noteable.to_reference,
branch: noteable.source_branch
}
else
{}
end
end
def noteable_name(noteable) def noteable_name(noteable)
name = noteable.model_name.singular name = noteable.model_name.singular
......
...@@ -8,9 +8,10 @@ ...@@ -8,9 +8,10 @@
= markdown @service.help = markdown @service.help
.service-settings .service-settings
.js-vue-integration-settings{ data: { show_active: @service.show_active_box?.to_s, activated: (@service.active || @service.new_record?).to_s } } .js-vue-integration-settings{ data: { show_active: @service.show_active_box?.to_s, activated: (@service.active || @service.new_record?).to_s, type: @service.to_param, merge_request_events: @service.merge_requests_events.to_s,
commit_events: @service.commit_events.to_s, enable_comments: @service.comment_on_event_enabled.to_s, comment_detail: @service.comment_detail } }
- if @service.configurable_events.present? - if @service.configurable_events.present? && !@service.is_a?(JiraService)
.form-group.row .form-group.row
%label.col-form-label.col-sm-2= _('Trigger') %label.col-form-label.col-sm-2= _('Trigger')
...@@ -31,22 +32,6 @@ ...@@ -31,22 +32,6 @@
%p.text-muted %p.text-muted
= @service.class.event_description(event) = @service.class.event_description(event)
- if @service.configurable_event_actions.present?
.form-group.row
%label.col-form-label.col-sm-2= _('Event Actions')
.col-sm-10
- @service.configurable_event_actions.each do |action|
.form-group
.form-check
= form.check_box service_event_action_field_name(action), class: 'form-check-input'
= form.label service_event_action_field_name(action), class: 'form-check-label' do
%strong
= event_action_description(action)
%p.text-muted
= event_action_description(action)
- @service.global_fields.each do |field| - @service.global_fields.each do |field|
- type = field[:type] - type = field[:type]
......
---
title: Update Jira comment to include more information
merge_request: 30258
author:
type: added
...@@ -8572,9 +8572,6 @@ msgstr "" ...@@ -8572,9 +8572,6 @@ msgstr ""
msgid "Estimated" msgid "Estimated"
msgstr "" msgstr ""
msgid "Event Actions"
msgstr ""
msgid "EventFilterBy|Filter by all" msgid "EventFilterBy|Filter by all"
msgstr "" msgstr ""
...@@ -11458,6 +11455,30 @@ msgstr "" ...@@ -11458,6 +11455,30 @@ msgstr ""
msgid "Integrations allow you to integrate GitLab with other applications" msgid "Integrations allow you to integrate GitLab with other applications"
msgstr "" msgstr ""
msgid "Integrations|All details"
msgstr ""
msgid "Integrations|Comment detail:"
msgstr ""
msgid "Integrations|Comment settings:"
msgstr ""
msgid "Integrations|Enable comments"
msgstr ""
msgid "Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs"
msgstr ""
msgid "Integrations|Includes commit title and branch"
msgstr ""
msgid "Integrations|Standard"
msgstr ""
msgid "Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) will be created."
msgstr ""
msgid "Interested parties can even contribute by pushing commits if they want to." msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr "" msgstr ""
...@@ -11761,6 +11782,12 @@ msgstr "" ...@@ -11761,6 +11782,12 @@ msgstr ""
msgid "Jira project: %{importProject}" msgid "Jira project: %{importProject}"
msgstr "" msgstr ""
msgid "JiraService| on branch %{branch_link}"
msgstr ""
msgid "JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}"
msgstr ""
msgid "JiraService|Events for %{noteable_model_name} are disabled." msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr "" msgstr ""
......
...@@ -9,7 +9,6 @@ describe('ActiveToggle', () => { ...@@ -9,7 +9,6 @@ describe('ActiveToggle', () => {
const defaultProps = { const defaultProps = {
initialActivated: true, initialActivated: true,
disabled: false,
}; };
const createComponent = props => { const createComponent = props => {
......
import { shallowMount } from '@vue/test-utils';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
describe('IntegrationForm', () => {
let wrapper;
const defaultProps = {
activeToggleProps: {
initialActivated: true,
},
showActive: true,
triggerFieldsProps: {
initialTriggerCommit: false,
initialTriggerMergeRequest: false,
initialEnableComments: false,
},
type: '',
};
const createComponent = props => {
wrapper = shallowMount(IntegrationForm, {
propsData: { ...defaultProps, ...props },
stubs: {
ActiveToggle,
JiraTriggerFields,
},
});
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const findActiveToggle = () => wrapper.find(ActiveToggle);
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
describe('template', () => {
describe('showActive is true', () => {
it('renders ActiveToggle', () => {
createComponent();
expect(findActiveToggle().exists()).toBe(true);
});
});
describe('showActive is false', () => {
it('does not render ActiveToggle', () => {
createComponent({
showActive: false,
});
expect(findActiveToggle().exists()).toBe(false);
});
});
describe('type is "slack"', () => {
it('does not render JiraTriggerFields', () => {
createComponent({
type: 'slack',
});
expect(findJiraTriggerFields().exists()).toBe(false);
});
});
describe('type is "jira"', () => {
it('renders JiraTriggerFields', () => {
createComponent({
type: 'jira',
});
expect(findJiraTriggerFields().exists()).toBe(true);
});
});
});
});
import { mount } from '@vue/test-utils';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import { GlFormCheckbox } from '@gitlab/ui';
describe('JiraTriggerFields', () => {
let wrapper;
const defaultProps = {
initialTriggerCommit: false,
initialTriggerMergeRequest: false,
initialEnableComments: false,
};
const createComponent = props => {
wrapper = mount(JiraTriggerFields, {
propsData: Object.assign({}, defaultProps, props),
});
};
afterEach(() => {
if (wrapper) wrapper.destroy();
});
const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]');
const findCommentDetail = () => wrapper.find('[data-testid="comment-detail"]');
const findCommentSettingsCheckbox = () => findCommentSettings().find(GlFormCheckbox);
describe('template', () => {
describe('initialTriggerCommit and initialTriggerMergeRequest are false', () => {
it('does not show comment settings', () => {
createComponent();
expect(findCommentSettings().isVisible()).toBe(false);
expect(findCommentDetail().isVisible()).toBe(false);
});
});
describe('initialTriggerCommit is true', () => {
beforeEach(() => {
createComponent({
initialTriggerCommit: true,
});
});
it('shows comment settings', () => {
expect(findCommentSettings().isVisible()).toBe(true);
expect(findCommentDetail().isVisible()).toBe(false);
});
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
// browsers don't include unchecked boxes in form submissions.
it('includes comment settings as false even if unchecked', () => {
expect(
findCommentSettings()
.find('input[name="service[comment_on_event_enabled]"]')
.exists(),
).toBe(true);
});
describe('on enable comments', () => {
it('shows comment detail', () => {
findCommentSettingsCheckbox().vm.$emit('input', true);
return wrapper.vm.$nextTick().then(() => {
expect(findCommentDetail().isVisible()).toBe(true);
});
});
});
});
describe('initialTriggerMergeRequest is true', () => {
it('shows comment settings', () => {
createComponent({
initialTriggerMergeRequest: true,
});
expect(findCommentSettings().isVisible()).toBe(true);
expect(findCommentDetail().isVisible()).toBe(false);
});
});
describe('initialTriggerCommit is true, initialEnableComments is true', () => {
it('shows comment settings and comment detail', () => {
createComponent({
initialTriggerCommit: true,
initialEnableComments: true,
});
expect(findCommentSettings().isVisible()).toBe(true);
expect(findCommentDetail().isVisible()).toBe(true);
});
});
});
});
...@@ -582,6 +582,79 @@ describe JiraService do ...@@ -582,6 +582,79 @@ describe JiraService do
end end
end end
describe '#create_cross_reference_note' do
let_it_be(:user) { build_stubbed(:user) }
let_it_be(:project) { create(:project, :repository) }
let(:jira_service) do
described_class.new(
project: project,
url: url,
username: username,
password: password
)
end
let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
subject { jira_service.create_cross_reference_note(jira_issue, resource, user) }
shared_examples 'creates a comment on Jira' do
let(:issue_url) { "#{url}/rest/api/2/issue/JIRA-123" }
let(:comment_url) { "#{issue_url}/comment" }
let(:remote_link_url) { "#{issue_url}/remotelink" }
before do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
stub_request(:get, issue_url).with(basic_auth: [username, password])
stub_request(:post, comment_url).with(basic_auth: [username, password])
stub_request(:post, remote_link_url).with(basic_auth: [username, password])
end
it 'creates a comment on Jira' do
subject
expect(WebMock).to have_requested(:post, comment_url).with(
body: /mentioned this issue in/
).once
end
end
context 'when resource is a commit' do
let(:resource) { project.commit('master') }
context 'when disabled' do
before do
allow_next_instance_of(JiraService) do |instance|
allow(instance).to receive(:commit_events) { false }
end
end
it { is_expected.to eq('Events for commits are disabled.') }
end
context 'when enabled' do
it_behaves_like 'creates a comment on Jira'
end
end
context 'when resource is a merge request' do
let(:resource) { build_stubbed(:merge_request, source_project: project) }
context 'when disabled' do
before do
allow_next_instance_of(JiraService) do |instance|
allow(instance).to receive(:merge_requests_events) { false }
end
end
it { is_expected.to eq('Events for merge requests are disabled.') }
end
context 'when enabled' do
it_behaves_like 'creates a comment on Jira'
end
end
end
describe '#test' do describe '#test' do
let(:jira_service) do let(:jira_service) do
described_class.new( described_class.new(
......
...@@ -462,7 +462,8 @@ describe SystemNoteService do ...@@ -462,7 +462,8 @@ describe SystemNoteService do
describe "existing reference" do describe "existing reference" do
before do before do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.full_path}|http://localhost/#{project.full_path}/-/commit/#{commit.id}]:\n'#{commit.title.chomp}'" message = double('message')
allow(message).to receive(:include?) { true }
allow_next_instance_of(JIRA::Resource::Issue) do |instance| allow_next_instance_of(JIRA::Resource::Issue) do |instance|
allow(instance).to receive(:comments).and_return([OpenStruct.new(body: message)]) allow(instance).to receive(:comments).and_return([OpenStruct.new(body: message)])
end end
......
...@@ -29,20 +29,5 @@ describe 'projects/services/_form' do ...@@ -29,20 +29,5 @@ describe 'projects/services/_form' do
expect(rendered).to have_content('Event will be triggered when a commit is created/updated') expect(rendered).to have_content('Event will be triggered when a commit is created/updated')
expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged') expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged')
end end
context 'when service is Jira' do
let(:project) { create(:jira_project) }
before do
assign(:service, project.jira_service)
end
it 'display merge_request_events and commit_events descriptions' do
render
expect(rendered).to have_content('Jira comments will be created when an issue gets referenced in a commit.')
expect(rendered).to have_content('Jira comments will be created when an issue gets referenced in a merge request.')
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment