Commit 50e1dfbe authored by Illya Klymov's avatar Illya Klymov

Merge branch '16119-jira-issue-transition-automatic' into 'master'

Support automatic transitions of Jira issues

See merge request gitlab-org/gitlab!53760
parents c1de8637 28e8e5fc
<script> <script>
import { GlFormGroup, GlFormCheckbox, GlFormRadio } from '@gitlab/ui'; import {
GlFormGroup,
GlFormCheckbox,
GlFormRadio,
GlFormInput,
GlLink,
GlSprintf,
} from '@gitlab/ui';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import eventHub from '../event_hub';
const commentDetailOptions = [ const commentDetailOptions = [
{ {
...@@ -18,12 +27,41 @@ const commentDetailOptions = [ ...@@ -18,12 +27,41 @@ const commentDetailOptions = [
}, },
]; ];
const ISSUE_TRANSITION_AUTO = 'auto';
const ISSUE_TRANSITION_CUSTOM = 'custom';
const issueTransitionOptions = [
{
value: ISSUE_TRANSITION_AUTO,
label: s__('JiraService|Move to Done'),
help: s__(
'JiraService|Automatically transitions Jira issues to the "Done" category. %{linkStart}Learn more%{linkEnd}',
),
link: helpPagePath('user/project/integrations/jira.html', {
anchor: 'automatic-issue-transitions',
}),
},
{
value: ISSUE_TRANSITION_CUSTOM,
label: s__('JiraService|Use custom transitions'),
help: s__(
'JiraService|Set a custom final state by using transition IDs. %{linkStart}Learn about transition IDs%{linkEnd}',
),
link: helpPagePath('user/project/integrations/jira.html', {
anchor: 'custom-issue-transitions',
}),
},
];
export default { export default {
name: 'JiraTriggerFields', name: 'JiraTriggerFields',
components: { components: {
GlFormGroup, GlFormGroup,
GlFormCheckbox, GlFormCheckbox,
GlFormRadio, GlFormRadio,
GlFormInput,
GlLink,
GlSprintf,
}, },
props: { props: {
initialTriggerCommit: { initialTriggerCommit: {
...@@ -43,21 +81,52 @@ export default { ...@@ -43,21 +81,52 @@ export default {
required: false, required: false,
default: 'standard', default: 'standard',
}, },
initialJiraIssueTransitionId: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
return { return {
validated: false,
triggerCommit: this.initialTriggerCommit, triggerCommit: this.initialTriggerCommit,
triggerMergeRequest: this.initialTriggerMergeRequest, triggerMergeRequest: this.initialTriggerMergeRequest,
enableComments: this.initialEnableComments, enableComments: this.initialEnableComments,
commentDetail: this.initialCommentDetail, commentDetail: this.initialCommentDetail,
jiraIssueTransitionId: this.initialJiraIssueTransitionId,
issueTransitionMode: this.initialJiraIssueTransitionId.length
? ISSUE_TRANSITION_CUSTOM
: ISSUE_TRANSITION_AUTO,
commentDetailOptions, commentDetailOptions,
issueTransitionOptions,
}; };
}, },
computed: { computed: {
...mapGetters(['isInheriting']), ...mapGetters(['isInheriting']),
showEnableComments() { showTriggerSettings() {
return this.triggerCommit || this.triggerMergeRequest; return this.triggerCommit || this.triggerMergeRequest;
}, },
validIssueTransitionId() {
return !this.validated || this.jiraIssueTransitionId.length > 0;
},
},
created() {
eventHub.$on('validateForm', this.validateForm);
},
beforeDestroy() {
eventHub.$off('validateForm', this.validateForm);
},
methods: {
validateForm() {
this.validated = true;
},
showCustomIssueTransitions(currentOption) {
return (
this.issueTransitionMode === ISSUE_TRANSITION_CUSTOM &&
currentOption === ISSUE_TRANSITION_CUSTOM
);
},
}, },
}; };
</script> </script>
...@@ -89,7 +158,7 @@ export default { ...@@ -89,7 +158,7 @@ export default {
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
v-show="showEnableComments" v-show="showTriggerSettings"
:label="s__('Integrations|Comment settings:')" :label="s__('Integrations|Comment settings:')"
label-for="service[comment_on_event_enabled]" label-for="service[comment_on_event_enabled]"
class="gl-pl-6" class="gl-pl-6"
...@@ -106,7 +175,7 @@ export default { ...@@ -106,7 +175,7 @@ export default {
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
v-show="showEnableComments && enableComments" v-show="showTriggerSettings && enableComments"
:label="s__('Integrations|Comment detail:')" :label="s__('Integrations|Comment detail:')"
label-for="service[comment_detail]" label-for="service[comment_detail]"
class="gl-pl-9" class="gl-pl-9"
...@@ -126,5 +195,51 @@ export default { ...@@ -126,5 +195,51 @@ export default {
</template> </template>
</gl-form-radio> </gl-form-radio>
</gl-form-group> </gl-form-group>
<gl-form-group
v-show="showTriggerSettings"
:label="s__('JiraService|Transition Jira issues to their final state:')"
class="gl-pl-6"
data-testid="issue-transition-settings"
>
<input type="hidden" name="service[jira_issue_transition_id]" value="" />
<gl-form-radio
v-for="issueTransitionOption in issueTransitionOptions"
:key="issueTransitionOption.value"
v-model="issueTransitionMode"
:value="issueTransitionOption.value"
:disabled="isInheriting"
:data-qa-selector="`service_issue_transition_mode_${issueTransitionOption.value}`"
>
{{ issueTransitionOption.label }}
<template v-if="showCustomIssueTransitions(issueTransitionOption.value)">
<gl-form-input
v-model="jiraIssueTransitionId"
name="service[jira_issue_transition_id]"
type="text"
class="gl-my-3"
data-qa-selector="service_jira_issue_transition_id_field"
:placeholder="s__('JiraService|For example, 12, 24')"
:disabled="isInheriting"
:required="true"
:state="validIssueTransitionId"
/>
<span class="invalid-feedback">
{{ s__('This field is required.') }}
</span>
</template>
<template #help>
<gl-sprintf :message="issueTransitionOption.help">
<template #link="{ content }">
<gl-link :href="issueTransitionOption.link" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
</gl-form-radio>
</gl-form-group>
</div> </div>
</template> </template>
...@@ -28,6 +28,7 @@ function parseDatasetToProps(data) { ...@@ -28,6 +28,7 @@ function parseDatasetToProps(data) {
testPath, testPath,
resetPath, resetPath,
vulnerabilitiesIssuetype, vulnerabilitiesIssuetype,
jiraIssueTransitionId,
...booleanAttributes ...booleanAttributes
} = data; } = data;
const { const {
...@@ -59,6 +60,7 @@ function parseDatasetToProps(data) { ...@@ -59,6 +60,7 @@ function parseDatasetToProps(data) {
initialTriggerMergeRequest: mergeRequestEvents, initialTriggerMergeRequest: mergeRequestEvents,
initialEnableComments: enableComments, initialEnableComments: enableComments,
initialCommentDetail: commentDetail, initialCommentDetail: commentDetail,
initialJiraIssueTransitionId: jiraIssueTransitionId,
}, },
jiraIssuesProps: { jiraIssuesProps: {
showJiraIssuesIntegration, showJiraIssuesIntegration,
......
...@@ -86,7 +86,7 @@ module ServicesHelper ...@@ -86,7 +86,7 @@ module ServicesHelper
end end
def integration_form_data(integration, group: nil) def integration_form_data(integration, group: nil)
{ form_data = {
id: integration.id, id: integration.id,
show_active: integration.show_active_box?.to_s, show_active: integration.show_active_box?.to_s,
activated: (integration.active || integration.new_record?).to_s, activated: (integration.active || integration.new_record?).to_s,
...@@ -106,6 +106,12 @@ module ServicesHelper ...@@ -106,6 +106,12 @@ module ServicesHelper
test_path: scoped_test_integration_path(integration), test_path: scoped_test_integration_path(integration),
reset_path: scoped_reset_integration_path(integration, group: group) reset_path: scoped_reset_integration_path(integration, group: group)
} }
if integration.is_a?(JiraService)
form_data[:jira_issue_transition_id] = integration.jira_issue_transition_id
end
form_data
end end
def trigger_events_for_service(integration) def trigger_events_for_service(integration)
......
...@@ -124,15 +124,11 @@ class JiraService < IssueTrackerService ...@@ -124,15 +124,11 @@ class JiraService < IssueTrackerService
end end
def fields def fields
transition_id_help_path = help_page_path('user/project/integrations/jira', anchor: 'obtaining-a-transition-id')
transition_id_help_link_start = '<a href="%{transition_id_help_path}" target="_blank" rel="noopener noreferrer">'.html_safe % { transition_id_help_path: transition_id_help_path }
[ [
{ type: 'text', name: 'url', title: s_('JiraService|Web URL'), placeholder: 'https://jira.example.com', required: true }, { type: 'text', name: 'url', title: s_('JiraService|Web URL'), placeholder: 'https://jira.example.com', required: true },
{ type: 'text', name: 'api_url', title: s_('JiraService|Jira API URL'), placeholder: s_('JiraService|If different from Web URL') }, { type: 'text', name: 'api_url', title: s_('JiraService|Jira API URL'), placeholder: s_('JiraService|If different from Web URL') },
{ type: 'text', name: 'username', title: s_('JiraService|Username or Email'), placeholder: s_('JiraService|Use a username for server version and an email for cloud version'), required: true }, { type: 'text', name: 'username', title: s_('JiraService|Username or Email'), placeholder: s_('JiraService|Use a username for server version and an email for cloud version'), required: true },
{ type: 'password', name: 'password', title: s_('JiraService|Password or API token'), placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'), required: true }, { type: 'password', name: 'password', title: s_('JiraService|Password or API token'), placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'), required: true }
{ type: 'text', name: 'jira_issue_transition_id', title: s_('JiraService|Jira workflow transition IDs'), placeholder: s_('JiraService|For example, 12, 24'), help: s_('JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}'.html_safe % { link_start: transition_id_help_link_start, link_end: '</a>'.html_safe }) }
] ]
end end
...@@ -159,17 +155,19 @@ class JiraService < IssueTrackerService ...@@ -159,17 +155,19 @@ class JiraService < IssueTrackerService
# support any events. # support any events.
end end
def find_issue(issue_key, rendered_fields: false) def find_issue(issue_key, rendered_fields: false, transitions: false)
options = {} expands = []
options = options.merge(expand: 'renderedFields') if rendered_fields expands << 'renderedFields' if rendered_fields
expands << 'transitions' if transitions
options = { expand: expands.join(',') } if expands.any?
jira_request { client.Issue.find(issue_key, options) } jira_request { client.Issue.find(issue_key, options || {}) }
end end
def close_issue(entity, external_issue, current_user) def close_issue(entity, external_issue, current_user)
issue = find_issue(external_issue.iid) issue = find_issue(external_issue.iid, transitions: automatic_issue_transitions?)
return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? return if issue.nil? || has_resolution?(issue)
commit_id = case entity commit_id = case entity
when Commit then entity.id when Commit then entity.id
...@@ -260,24 +258,52 @@ class JiraService < IssueTrackerService ...@@ -260,24 +258,52 @@ class JiraService < IssueTrackerService
end end
end end
def automatic_issue_transitions?
jira_issue_transition_id.blank?
end
# jira_issue_transition_id can have multiple values split by , or ; # jira_issue_transition_id can have multiple values split by , or ;
# the issue is transitioned at the order given by the user # the issue is transitioned at the order given by the user
# if any transition fails it will log the error message and stop the transition sequence # if any transition fails it will log the error message and stop the transition sequence
def transition_issue(issue) def transition_issue(issue)
jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).each do |transition_id| return transition_issue_to_done(issue) if automatic_issue_transitions?
issue.transitions.build.save!(transition: { id: transition_id })
rescue => error jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id|
log_error( transition_issue_to_id(issue, transition_id)
"Issue transition failed", end
error: { end
exception_class: error.class.name,
exception_message: error.message, def transition_issue_to_id(issue, transition_id)
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace) issue.transitions.build.save!(
}, transition: { id: transition_id }
client_url: client_url )
)
return false true
rescue => error
log_error(
"Issue transition failed",
error: {
exception_class: error.class.name,
exception_message: error.message,
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
},
client_url: client_url
)
false
end
def transition_issue_to_done(issue)
transitions = issue.transitions rescue []
transition = transitions.find do |transition|
status = transition&.to&.statusCategory
status && status['key'] == 'done'
end end
return false unless transition
transition_issue_to_id(issue, transition.id)
end end
def log_usage(action, user) def log_usage(action, user)
......
---
title: Support automatic transitions of Jira issues
merge_request: 53760
author:
type: changed
...@@ -814,7 +814,7 @@ Parameters: ...@@ -814,7 +814,7 @@ Parameters:
| `username` | string | yes | The username of the user created to be used with GitLab/Jira. | | `username` | string | yes | The username of the user created to be used with GitLab/Jira. |
| `password` | string | yes | The password of the user created to be used with GitLab/Jira. | | `password` | string | yes | The password of the user created to be used with GitLab/Jira. |
| `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). | | `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). |
| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the transitions ID column. By default, this ID is set to `2`. | | `jira_issue_transition_id` | string | no | The ID of one or more transitions to move issues to a closed state. Read [custom issue transitions](../user/project/integrations/jira.md#custom-issue-transitions) for details. Defaults to a blank string, which enables [automatic issue transitions](../user/project/integrations/jira.md#automatic-issue-transitions). |
| `commit_events` | boolean | false | Enable notifications for commit events | | `commit_events` | boolean | false | Enable notifications for commit events |
| `merge_requests_events` | boolean | false | Enable notifications for merge request events | | `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `comment_on_event_enabled` | boolean | false | Enable comments inside Jira issues on each GitLab event (commit / merge request) | | `comment_on_event_enabled` | boolean | false | Enable comments inside Jira issues on each GitLab event (commit / merge request) |
......
...@@ -106,8 +106,6 @@ To enable the Jira integration in a project: ...@@ -106,8 +106,6 @@ To enable the Jira integration in a project:
1. To include a comment on the Jira issue when the above reference is made in GitLab, select 1. To include a comment on the Jira issue when the above reference is made in GitLab, select
**Enable comments**. **Enable comments**.
1. Select the **Comment detail**: **Standard** or **All details**.
1. Enter the further details on the page as described in the following table. 1. Enter the further details on the page as described in the following table.
| Field | Description | | Field | Description |
...@@ -116,7 +114,6 @@ To enable the Jira integration in a project: ...@@ -116,7 +114,6 @@ To enable the Jira integration in a project:
| `Jira API URL` | The base URL to the Jira instance API. Web URL value is used if not set. For example, `https://jira-api.example.com`. Leave this field blank (or use the same value of `Web URL`) if using **Jira on Atlassian cloud**. | | `Jira API URL` | The base URL to the Jira instance API. Web URL value is used if not set. For example, `https://jira-api.example.com`. Leave this field blank (or use the same value of `Web URL`) if using **Jira on Atlassian cloud**. |
| `Username or Email` | Created in [configure Jira](#configure-jira) step. Use `username` for **Jira Server** or `email` for **Jira on Atlassian cloud**. | | `Username or Email` | Created in [configure Jira](#configure-jira) step. Use `username` for **Jira Server** or `email` for **Jira on Atlassian cloud**. |
| `Password/API token` | Created in [configure Jira](#configure-jira) step. Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**. | | `Password/API token` | Created in [configure Jira](#configure-jira) step. Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**. |
| `Jira workflow transition IDs` | Required for closing Jira issues via commits or merge requests. These are the IDs of transitions in Jira that move issues to a particular state. (See [Obtaining a transition ID](#obtaining-a-transition-id).) If you insert multiple transition IDs separated by `,` or `;`, the issue is moved to each state, one after another, using the given order. In GitLab 13.6 and earlier, field was called `Transition ID`. |
1. To enable users to view Jira issues inside the GitLab project, select **Enable Jira issues** and 1. To enable users to view Jira issues inside the GitLab project, select **Enable Jira issues** and
enter a Jira project key. **(PREMIUM)** enter a Jira project key. **(PREMIUM)**
...@@ -138,10 +135,19 @@ To enable the Jira integration in a project: ...@@ -138,10 +135,19 @@ To enable the Jira integration in a project:
Your GitLab project can now interact with all Jira projects in your instance and the project now Your GitLab project can now interact with all Jira projects in your instance and the project now
displays a Jira link that opens the Jira project. displays a Jira link that opens the Jira project.
#### Obtaining a transition ID #### Automatic issue transitions
When you [close a Jira issues with a trigger word](../issues/managing_issues.md#closing-issues-automatically),
GitLab by default transitions the issue to the next available status with a category of "Done".
#### Custom issue transitions
For advanced workflows you can specify custom Jira transition IDs. If you insert multiple transition IDs separated by `,` or `;`, the issue is moved to each state, one after another, using the given order.
To see the transition IDs on Jira Cloud, edit a workflow in the **Text** view.
The transition IDs display in the **Transitions** column.
In the most recent Jira user interface, you can no longer see transition IDs in the workflow On Jira Server you can get the transition IDs in either of the following ways:
administration UI. You can get the ID you need in either of the following ways:
1. By using the API, with a request like `https://yourcompany.atlassian.net/rest/api/2/issue/ISSUE-123/transitions` 1. By using the API, with a request like `https://yourcompany.atlassian.net/rest/api/2/issue/ISSUE-123/transitions`
using an issue that is in the appropriate "open" state using an issue that is in the appropriate "open" state
......
...@@ -18,6 +18,20 @@ RSpec.describe EE::ServicesHelper do ...@@ -18,6 +18,20 @@ RSpec.describe EE::ServicesHelper do
subject { controller_class.new } subject { controller_class.new }
describe '#integration_form_data' do describe '#integration_form_data' do
let(:jira_fields) do
{
show_jira_issues_integration: 'false',
show_jira_vulnerabilities_integration: 'false',
enable_jira_issues: 'true',
enable_jira_vulnerabilities: 'false',
project_key: 'FE',
vulnerabilities_issuetype: '10001',
gitlab_issues_enabled: 'true',
upgrade_plan_path: nil,
edit_project_path: edit_project_path(project, anchor: 'js-shared-permissions')
}
end
subject { helper.integration_form_data(integration) } subject { helper.integration_form_data(integration) }
before do before do
...@@ -28,7 +42,7 @@ RSpec.describe EE::ServicesHelper do ...@@ -28,7 +42,7 @@ RSpec.describe EE::ServicesHelper do
let(:integration) { build(:slack_service) } let(:integration) { build(:slack_service) }
it 'does not include Jira specific fields' do it 'does not include Jira specific fields' do
is_expected.not_to include(:show_jira_issues_integration, :show_jira_vulnerabilities_integration, :enable_jira_issues, :project_key, :gitlab_issues_enabled, :edit_project_path) is_expected.not_to include(*jira_fields.keys)
end end
end end
...@@ -40,8 +54,8 @@ RSpec.describe EE::ServicesHelper do ...@@ -40,8 +54,8 @@ RSpec.describe EE::ServicesHelper do
allow(integration).to receive(:jira_vulnerabilities_integration_available?).and_return(false) allow(integration).to receive(:jira_vulnerabilities_integration_available?).and_return(false)
end end
it 'includes Jira specific fields' do it 'includes default Jira fields' do
is_expected.to include(show_jira_vulnerabilities_integration: 'false') is_expected.to include(jira_fields)
end end
end end
...@@ -51,8 +65,8 @@ RSpec.describe EE::ServicesHelper do ...@@ -51,8 +65,8 @@ RSpec.describe EE::ServicesHelper do
stub_feature_flags(jira_for_vulnerabilities: false) stub_feature_flags(jira_for_vulnerabilities: false)
end end
it 'includes Jira specific fields' do it 'includes show_jira_issues_integration' do
is_expected.to include(show_jira_vulnerabilities_integration: 'false') is_expected.to include(jira_fields.merge(show_jira_issues_integration: 'true'))
end end
end end
...@@ -62,16 +76,13 @@ RSpec.describe EE::ServicesHelper do ...@@ -62,16 +76,13 @@ RSpec.describe EE::ServicesHelper do
stub_feature_flags(jira_for_vulnerabilities: true) stub_feature_flags(jira_for_vulnerabilities: true)
end end
it 'includes Jira specific fields' do it 'includes all Jira fields' do
is_expected.to include( is_expected.to include(
show_jira_issues_integration: 'true', jira_fields.merge(
show_jira_vulnerabilities_integration: 'true', show_jira_issues_integration: 'true',
enable_jira_issues: 'true', show_jira_vulnerabilities_integration: 'true',
enable_jira_vulnerabilities: 'true', enable_jira_vulnerabilities: 'true'
project_key: 'FE', )
vulnerabilities_issuetype: '10001',
gitlab_issues_enabled: 'true',
edit_project_path: edit_project_path(project, anchor: 'js-shared-permissions')
) )
end end
end end
......
...@@ -16822,6 +16822,9 @@ msgstr "" ...@@ -16822,6 +16822,9 @@ msgstr ""
msgid "JiraService|An error occured while fetching issue list" msgid "JiraService|An error occured while fetching issue list"
msgstr "" msgstr ""
msgid "JiraService|Automatically transitions Jira issues to the \"Done\" category. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "JiraService|Define the type of Jira issue to create from a vulnerability." msgid "JiraService|Define the type of Jira issue to create from a vulnerability."
msgstr "" msgstr ""
...@@ -16876,7 +16879,7 @@ msgstr "" ...@@ -16876,7 +16879,7 @@ msgstr ""
msgid "JiraService|Jira project key" msgid "JiraService|Jira project key"
msgstr "" msgstr ""
msgid "JiraService|Jira workflow transition IDs" msgid "JiraService|Move to Done"
msgstr "" msgstr ""
msgid "JiraService|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}Jira%{linkEnd}." msgid "JiraService|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}Jira%{linkEnd}."
...@@ -16897,7 +16900,7 @@ msgstr "" ...@@ -16897,7 +16900,7 @@ msgstr ""
msgid "JiraService|Select issue type" msgid "JiraService|Select issue type"
msgstr "" msgstr ""
msgid "JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}" msgid "JiraService|Set a custom final state by using transition IDs. %{linkStart}Learn about transition IDs%{linkEnd}"
msgstr "" msgstr ""
msgid "JiraService|Sign in to GitLab.com to get started." msgid "JiraService|Sign in to GitLab.com to get started."
...@@ -16909,12 +16912,18 @@ msgstr "" ...@@ -16909,12 +16912,18 @@ msgstr ""
msgid "JiraService|This issue is synchronized with Jira" msgid "JiraService|This issue is synchronized with Jira"
msgstr "" msgstr ""
msgid "JiraService|Transition Jira issues to their final state:"
msgstr ""
msgid "JiraService|Use a password for server version and an API token for cloud version" msgid "JiraService|Use a password for server version and an API token for cloud version"
msgstr "" msgstr ""
msgid "JiraService|Use a username for server version and an email for cloud version" msgid "JiraService|Use a username for server version and an email for cloud version"
msgstr "" msgstr ""
msgid "JiraService|Use custom transitions"
msgstr ""
msgid "JiraService|Username or Email" msgid "JiraService|Username or Email"
msgstr "" msgstr ""
......
...@@ -10,7 +10,12 @@ module QA ...@@ -10,7 +10,12 @@ module QA
element :service_url_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern element :service_url_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :service_username_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern element :service_username_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :service_password_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern element :service_password_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :service_jira_issue_transition_id_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern end
view 'app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue' do
element :service_issue_transition_mode_auto, ':data-qa-selector="`service_issue_transition_mode_${issueTransitionOption.value}`"' # rubocop:disable QA/ElementWithPattern
element :service_issue_transition_mode_custom, ':data-qa-selector="`service_issue_transition_mode_${issueTransitionOption.value}`"' # rubocop:disable QA/ElementWithPattern
element :service_jira_issue_transition_id_field
end end
view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do
...@@ -23,7 +28,9 @@ module QA ...@@ -23,7 +28,9 @@ module QA
set_jira_server_url(url) set_jira_server_url(url)
set_username(Runtime::Env.jira_admin_username) set_username(Runtime::Env.jira_admin_username)
set_password(Runtime::Env.jira_admin_password) set_password(Runtime::Env.jira_admin_password)
set_transaction_ids('11,21,31,41')
use_custom_transitions
set_transition_ids('11,21,31,41')
click_save_changes_button click_save_changes_button
wait_until(reload: false) do wait_until(reload: false) do
...@@ -45,8 +52,16 @@ module QA ...@@ -45,8 +52,16 @@ module QA
fill_element(:service_password_field, password) fill_element(:service_password_field, password)
end end
def set_transaction_ids(transaction_ids) def use_automatic_transitions
fill_element(:service_jira_issue_transition_id_field, transaction_ids) click_element :service_issue_transition_mode_auto
end
def use_custom_transitions
click_element :service_issue_transition_mode_custom
end
def set_transition_ids(transition_ids)
fill_element(:service_jira_issue_transition_id_field, transition_ids)
end end
def click_save_changes_button def click_save_changes_button
......
...@@ -6,12 +6,14 @@ RSpec.describe 'User activates Jira', :js do ...@@ -6,12 +6,14 @@ RSpec.describe 'User activates Jira', :js do
include_context 'project service activation' include_context 'project service activation'
include_context 'project service Jira context' include_context 'project service Jira context'
before do
server_info = { key: 'value' }.to_json
stub_request(:get, test_url).to_return(body: server_info)
end
describe 'user tests Jira Service' do describe 'user tests Jira Service' do
context 'when Jira connection test succeeds' do context 'when Jira connection test succeeds' do
before do before do
server_info = { key: 'value' }.to_json
stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
visit_project_integration('Jira') visit_project_integration('Jira')
fill_form fill_form
click_test_then_save_integration(expect_test_to_fail: false) click_test_then_save_integration(expect_test_to_fail: false)
...@@ -81,4 +83,40 @@ RSpec.describe 'User activates Jira', :js do ...@@ -81,4 +83,40 @@ RSpec.describe 'User activates Jira', :js do
end end
end end
end end
describe 'issue transition settings' do
it 'shows validation errors' do
visit_project_integration('Jira')
expect(page).to have_field('Move to Done', checked: true)
fill_form
choose 'Use custom transitions'
click_save_integration
within '[data-testid="issue-transition-settings"]' do
expect(page).to have_content('This field is required.')
end
fill_in 'service[jira_issue_transition_id]', with: '1, 2, 3'
click_save_integration
expect(page).to have_content('Jira settings saved and active.')
expect(project.reload.jira_service.jira_issue_transition_id).to eq('1, 2, 3')
end
it 'clears the transition IDs when using automatic transitions' do
create(:jira_service, project: project, jira_issue_transition_id: '1, 2, 3')
visit_project_integration('Jira')
expect(page).to have_field('Use custom transitions', checked: true)
expect(page).to have_field('service[jira_issue_transition_id]', with: '1, 2, 3')
choose 'Move to Done'
click_save_integration
expect(page).to have_content('Jira settings saved and active.')
expect(project.reload.jira_service.jira_issue_transition_id).to eq('')
end
end
end end
...@@ -30,14 +30,21 @@ describe('JiraTriggerFields', () => { ...@@ -30,14 +30,21 @@ describe('JiraTriggerFields', () => {
const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]'); const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]');
const findCommentDetail = () => wrapper.find('[data-testid="comment-detail"]'); const findCommentDetail = () => wrapper.find('[data-testid="comment-detail"]');
const findCommentSettingsCheckbox = () => findCommentSettings().find(GlFormCheckbox); const findCommentSettingsCheckbox = () => findCommentSettings().find(GlFormCheckbox);
const findIssueTransitionSettings = () =>
wrapper.find('[data-testid="issue-transition-settings"]');
const findIssueTransitionModeRadios = () =>
findIssueTransitionSettings().findAll('input[type="radio"]');
const findIssueTransitionIdsField = () =>
wrapper.find('input[type="text"][name="service[jira_issue_transition_id]"]');
describe('template', () => { describe('template', () => {
describe('initialTriggerCommit and initialTriggerMergeRequest are false', () => { describe('initialTriggerCommit and initialTriggerMergeRequest are false', () => {
it('does not show comment settings', () => { it('does not show trigger settings', () => {
createComponent(); createComponent();
expect(findCommentSettings().isVisible()).toBe(false); expect(findCommentSettings().isVisible()).toBe(false);
expect(findCommentDetail().isVisible()).toBe(false); expect(findCommentDetail().isVisible()).toBe(false);
expect(findIssueTransitionSettings().isVisible()).toBe(false);
}); });
}); });
...@@ -48,9 +55,10 @@ describe('JiraTriggerFields', () => { ...@@ -48,9 +55,10 @@ describe('JiraTriggerFields', () => {
}); });
}); });
it('shows comment settings', () => { it('shows trigger settings', () => {
expect(findCommentSettings().isVisible()).toBe(true); expect(findCommentSettings().isVisible()).toBe(true);
expect(findCommentDetail().isVisible()).toBe(false); expect(findCommentDetail().isVisible()).toBe(false);
expect(findIssueTransitionSettings().isVisible()).toBe(true);
}); });
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1, // As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
...@@ -73,13 +81,14 @@ describe('JiraTriggerFields', () => { ...@@ -73,13 +81,14 @@ describe('JiraTriggerFields', () => {
}); });
describe('initialTriggerMergeRequest is true', () => { describe('initialTriggerMergeRequest is true', () => {
it('shows comment settings', () => { it('shows trigger settings', () => {
createComponent({ createComponent({
initialTriggerMergeRequest: true, initialTriggerMergeRequest: true,
}); });
expect(findCommentSettings().isVisible()).toBe(true); expect(findCommentSettings().isVisible()).toBe(true);
expect(findCommentDetail().isVisible()).toBe(false); expect(findCommentDetail().isVisible()).toBe(false);
expect(findIssueTransitionSettings().isVisible()).toBe(true);
}); });
}); });
...@@ -95,7 +104,41 @@ describe('JiraTriggerFields', () => { ...@@ -95,7 +104,41 @@ describe('JiraTriggerFields', () => {
}); });
}); });
it('disables checkboxes and radios if inheriting', () => { describe('initialJiraIssueTransitionId is not set', () => {
it('uses automatic transitions', () => {
createComponent({
initialTriggerCommit: true,
});
const [radio1, radio2] = findIssueTransitionModeRadios().wrappers;
expect(radio1.element.checked).toBe(true);
expect(radio2.element.checked).toBe(false);
expect(findIssueTransitionIdsField().exists()).toBe(false);
});
});
describe('initialJiraIssueTransitionId is set', () => {
it('uses custom transitions', () => {
createComponent({
initialJiraIssueTransitionId: '1, 2, 3',
initialTriggerCommit: true,
});
const [radio1, radio2] = findIssueTransitionModeRadios().wrappers;
expect(radio1.element.checked).toBe(false);
expect(radio2.element.checked).toBe(true);
const field = findIssueTransitionIdsField();
expect(field.isVisible()).toBe(true);
expect(field.element).toMatchObject({
type: 'text',
value: '1, 2, 3',
});
});
});
it('disables input fields if inheriting', () => {
createComponent( createComponent(
{ {
initialTriggerCommit: true, initialTriggerCommit: true,
...@@ -104,12 +147,8 @@ describe('JiraTriggerFields', () => { ...@@ -104,12 +147,8 @@ describe('JiraTriggerFields', () => {
true, true,
); );
wrapper.findAll('[type=checkbox]').wrappers.forEach((checkbox) => { wrapper.findAll('[type=text], [type=checkbox], [type=radio]').wrappers.forEach((input) => {
expect(checkbox.attributes('disabled')).toBe('disabled'); expect(input.attributes('disabled')).toBe('disabled');
});
wrapper.findAll('[type=radio]').wrappers.forEach((radio) => {
expect(radio.attributes('disabled')).toBe('disabled');
}); });
}); });
}); });
......
...@@ -4,32 +4,49 @@ require 'spec_helper' ...@@ -4,32 +4,49 @@ require 'spec_helper'
RSpec.describe ServicesHelper do RSpec.describe ServicesHelper do
describe '#integration_form_data' do describe '#integration_form_data' do
let(:fields) do
[
:id,
:show_active,
:activated,
:type,
:merge_request_events,
:commit_events,
:enable_comments,
:comment_detail,
:learn_more_path,
:trigger_events,
:fields,
:inherit_from_id,
:integration_level,
:editable,
:cancel_path,
:can_test,
:test_path,
:reset_path
]
end
let(:jira_fields) { %i[jira_issue_transition_id] }
subject { helper.integration_form_data(integration) } subject { helper.integration_form_data(integration) }
context 'Jira service' do context 'Slack service' do
let(:integration) { build(:jira_service) } let(:integration) { build(:slack_service) }
it 'includes Jira specific fields' do it { is_expected.to include(*fields) }
is_expected.to include( it { is_expected.not_to include(*jira_fields) }
:id,
:show_active,
:activated,
:type,
:merge_request_events,
:commit_events,
:enable_comments,
:comment_detail,
:trigger_events,
:fields,
:inherit_from_id,
:integration_level
)
end
specify do specify do
expect(subject[:reset_path]).to eq(helper.scoped_reset_integration_path(integration)) expect(subject[:reset_path]).to eq(helper.scoped_reset_integration_path(integration))
end end
end end
context 'Jira service' do
let(:integration) { build(:jira_service) }
it { is_expected.to include(*fields, *jira_fields) }
end
end end
describe '#scoped_reset_integration_path' do describe '#scoped_reset_integration_path' do
......
...@@ -166,20 +166,6 @@ RSpec.describe MergeRequests::MergeService do ...@@ -166,20 +166,6 @@ RSpec.describe MergeRequests::MergeService do
service.execute(merge_request) service.execute(merge_request)
end end
context 'when jira_issue_transition_id is not present' do
before do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil)
end
it 'does not close issue' do
jira_tracker.update!(jira_issue_transition_id: nil)
expect_any_instance_of(JiraService).not_to receive(:transition_issue)
service.execute(merge_request)
end
end
context 'wrong issue markdown' do context 'wrong issue markdown' do
it 'does not close issues on Jira issue tracker' do it 'does not close issues on Jira issue tracker' do
jira_issue = ExternalIssue.new('#JIRA-123', project) jira_issue = ExternalIssue.new('#JIRA-123', project)
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_context 'project service Jira context' do RSpec.shared_context 'project service Jira context' do
let(:url) { 'http://jira.example.com' } let(:url) { 'https://jira.example.com' }
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' } let(:test_url) { 'https://jira.example.com/rest/api/2/serverInfo' }
def fill_form(disable: false) def fill_form(disable: false)
click_active_checkbox if disable click_active_checkbox if disable
...@@ -10,6 +10,5 @@ RSpec.shared_context 'project service Jira context' do ...@@ -10,6 +10,5 @@ RSpec.shared_context 'project service Jira context' do
fill_in 'service_url', with: url fill_in 'service_url', with: url
fill_in 'service_username', with: 'username' fill_in 'service_username', with: 'username'
fill_in 'service_password', with: 'password' fill_in 'service_password', with: 'password'
fill_in 'service_jira_issue_transition_id', with: '25'
end end
end end
...@@ -15,7 +15,10 @@ RSpec.shared_context 'project service activation' do ...@@ -15,7 +15,10 @@ RSpec.shared_context 'project service activation' do
def visit_project_integration(name) def visit_project_integration(name)
visit_project_integrations visit_project_integrations
click_link(name)
within('#content-body') do
click_link(name)
end
end end
def click_active_checkbox def click_active_checkbox
......
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