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 { parseBoolean } from '~/lib/utils/common_utils';
import ActiveToggle from './components/active_toggle.vue';
import IntegrationForm from './components/integration_form.vue';
export default el => {
if (!el) {
return null;
}
const { showActive: showActiveStr, activated: activatedStr } = el.dataset;
const showActive = parseBoolean(showActiveStr);
const activated = parseBoolean(activatedStr);
if (!showActive) {
return null;
function parseBooleanInData(data) {
const result = {};
Object.entries(data).forEach(([key, value]) => {
result[key] = parseBoolean(value);
});
return result;
}
const { type, commentDetail, ...booleanAttributes } = el.dataset;
const {
showActive,
activated,
commitEvents,
mergeRequestEvents,
enableComments,
} = parseBooleanInData(booleanAttributes);
return new Vue({
el,
render(createElement) {
return createElement(ActiveToggle, {
return createElement(IntegrationForm, {
props: {
initialActivated: activated,
activeToggleProps: {
initialActivated: activated,
},
showActive,
type,
triggerFieldsProps: {
initialTriggerCommit: commitEvents,
initialTriggerMergeRequest: mergeRequestEvents,
initialEnableComments: enableComments,
initialCommentDetail: commentDetail,
},
},
});
},
......
......@@ -177,6 +177,7 @@ class JiraService < IssueTrackerService
noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
noteable_type = noteable_name(noteable)
entity_url = build_entity_url(noteable_type, noteable_id)
entity_meta = build_entity_meta(noteable)
data = {
user: {
......@@ -185,12 +186,15 @@ class JiraService < IssueTrackerService
},
project: {
name: project.full_path,
url: resource_url(namespace_project_path(project.namespace, project)) # rubocop:disable Cop/ProjectPathHelper
url: resource_url(project_path(project))
},
entity: {
id: entity_meta[:id],
name: noteable_type.humanize.downcase,
url: entity_url,
title: noteable.title
title: noteable.title,
description: entity_meta[:description],
branch: entity_meta[:branch]
}
}
......@@ -264,14 +268,11 @@ class JiraService < IssueTrackerService
end
def add_comment(data, issue)
user_name = data[:user][:name]
user_url = data[:user][:url]
entity_name = data[:entity][:name]
entity_url = data[:entity][:url]
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_props = build_remote_link_props(url: entity_url, title: link_title)
......@@ -280,6 +281,37 @@ class JiraService < IssueTrackerService
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)
issue.respond_to?(:resolution) && issue.resolution.present?
end
......@@ -353,6 +385,23 @@ class JiraService < IssueTrackerService
)
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)
name = noteable.model_name.singular
......
......@@ -8,9 +8,10 @@
= markdown @service.help
.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
%label.col-form-label.col-sm-2= _('Trigger')
......@@ -31,22 +32,6 @@
%p.text-muted
= @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|
- type = field[:type]
......
---
title: Update Jira comment to include more information
merge_request: 30258
author:
type: added
......@@ -8572,9 +8572,6 @@ msgstr ""
msgid "Estimated"
msgstr ""
msgid "Event Actions"
msgstr ""
msgid "EventFilterBy|Filter by all"
msgstr ""
......@@ -11458,6 +11455,30 @@ msgstr ""
msgid "Integrations allow you to integrate GitLab with other applications"
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."
msgstr ""
......@@ -11761,6 +11782,12 @@ msgstr ""
msgid "Jira project: %{importProject}"
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."
msgstr ""
......
......@@ -9,7 +9,6 @@ describe('ActiveToggle', () => {
const defaultProps = {
initialActivated: true,
disabled: false,
};
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
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
let(:jira_service) do
described_class.new(
......
......@@ -462,7 +462,8 @@ describe SystemNoteService do
describe "existing reference" do
before do
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(instance).to receive(:comments).and_return([OpenStruct.new(body: message)])
end
......
......@@ -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 merge request is created/updated/merged')
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
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