Commit 8ca4e503 authored by Mike Greiling's avatar Mike Greiling

Merge branch '223787-add-controls-to-enable-viewing-jira-issues-within-gitlab' into 'master'

Add controls to enable viewing Jira issues within GitLab

See merge request gitlab-org/gitlab!35417
parents 061750ec 181d8e5e
<script> <script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ActiveToggle from './active_toggle.vue'; import ActiveToggle from './active_toggle.vue';
import JiraTriggerFields from './jira_trigger_fields.vue'; import JiraTriggerFields from './jira_trigger_fields.vue';
import JiraIssuesFields from './jira_issues_fields.vue';
import TriggerFields from './trigger_fields.vue'; import TriggerFields from './trigger_fields.vue';
import DynamicField from './dynamic_field.vue'; import DynamicField from './dynamic_field.vue';
...@@ -9,9 +11,11 @@ export default { ...@@ -9,9 +11,11 @@ export default {
components: { components: {
ActiveToggle, ActiveToggle,
JiraTriggerFields, JiraTriggerFields,
JiraIssuesFields,
TriggerFields, TriggerFields,
DynamicField, DynamicField,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
activeToggleProps: { activeToggleProps: {
type: Object, type: Object,
...@@ -25,6 +29,10 @@ export default { ...@@ -25,6 +29,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
jiraIssuesProps: {
type: Object,
required: true,
},
triggerEvents: { triggerEvents: {
type: Array, type: Array,
required: false, required: false,
...@@ -44,6 +52,9 @@ export default { ...@@ -44,6 +52,9 @@ export default {
isJira() { isJira() {
return this.type === 'jira'; return this.type === 'jira';
}, },
showJiraIssuesFields() {
return this.isJira && this.glFeatures.jiraIntegration;
},
}, },
}; };
</script> </script>
...@@ -54,5 +65,6 @@ export default { ...@@ -54,5 +65,6 @@ export default {
<jira-trigger-fields v-if="isJira" v-bind="triggerFieldsProps" /> <jira-trigger-fields v-if="isJira" v-bind="triggerFieldsProps" />
<trigger-fields v-else-if="triggerEvents.length" :events="triggerEvents" :type="type" /> <trigger-fields v-else-if="triggerEvents.length" :events="triggerEvents" :type="type" />
<dynamic-field v-for="field in fields" :key="field.name" v-bind="field" /> <dynamic-field v-for="field in fields" :key="field.name" v-bind="field" />
<jira-issues-fields v-if="showJiraIssuesFields" v-bind="jiraIssuesProps" />
</div> </div>
</template> </template>
<script>
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
export default {
name: 'JiraIssuesFields',
components: {
GlFormGroup,
GlFormCheckbox,
GlFormInput,
GlSprintf,
GlLink,
},
props: {
initialEnableJiraIssues: {
type: Boolean,
required: false,
},
initialProjectKey: {
type: String,
required: false,
default: null,
},
editProjectPath: {
type: String,
required: false,
default: null,
},
},
data() {
return {
enableJiraIssues: this.initialEnableJiraIssues,
projectKey: this.initialProjectKey,
};
},
};
</script>
<template>
<div>
<gl-form-group
:label="s__('JiraService|View Jira issues in GitLab')"
label-for="jira-issue-settings"
>
<div id="jira-issue-settings">
<p>
{{
s__(
'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of issues and view any issue as read-only.',
)
}}
</p>
<input name="service[issues_enabled]" type="hidden" value="false" />
<gl-form-checkbox v-model="enableJiraIssues" name="service[issues_enabled]">
{{ s__('JiraService|Enable Jira issues') }}
<template #help>
{{
s__(
'JiraService|Warning: All GitLab users that have access to this GitLab project will be able to view all issues from the Jira project specified below.',
)
}}
</template>
</gl-form-checkbox>
</div>
</gl-form-group>
<gl-form-group :label="s__('JiraService|Jira project key')">
<gl-form-input
v-model="projectKey"
type="text"
name="service[project_key]"
:placeholder="s__('JiraService|e.g. AB')"
:disabled="!enableJiraIssues"
/>
</gl-form-group>
<p>
<gl-sprintf
:message="
s__(
'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.',
)
"
>
<template #link="{ content }">
<gl-link :href="editProjectPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</div>
</template>
...@@ -15,13 +15,22 @@ export default el => { ...@@ -15,13 +15,22 @@ export default el => {
return result; return result;
} }
const { type, commentDetail, triggerEvents, fields, ...booleanAttributes } = el.dataset; const {
type,
commentDetail,
projectKey,
editProjectPath,
triggerEvents,
fields,
...booleanAttributes
} = el.dataset;
const { const {
showActive, showActive,
activated, activated,
commitEvents, commitEvents,
mergeRequestEvents, mergeRequestEvents,
enableComments, enableComments,
enableJiraIssues,
} = parseBooleanInData(booleanAttributes); } = parseBooleanInData(booleanAttributes);
return new Vue({ return new Vue({
...@@ -40,6 +49,11 @@ export default el => { ...@@ -40,6 +49,11 @@ export default el => {
initialEnableComments: enableComments, initialEnableComments: enableComments,
initialCommentDetail: commentDetail, initialCommentDetail: commentDetail,
}, },
jiraIssuesProps: {
initialEnableJiraIssues: enableJiraIssues,
initialProjectKey: projectKey,
editProjectPath,
},
triggerEvents: JSON.parse(triggerEvents), triggerEvents: JSON.parse(triggerEvents),
fields: JSON.parse(fields), fields: JSON.parse(fields),
}, },
......
...@@ -13,6 +13,7 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -13,6 +13,7 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :redirect_deprecated_prometheus_service, only: [:update] before_action :redirect_deprecated_prometheus_service, only: [:update]
before_action only: :edit do before_action only: :edit do
push_frontend_feature_flag(:integration_form_refactor, default_enabled: true) push_frontend_feature_flag(:integration_form_refactor, default_enabled: true)
push_frontend_feature_flag(:jira_integration, @project)
end end
respond_to :html respond_to :html
......
...@@ -5,6 +5,7 @@ module EE ...@@ -5,6 +5,7 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
ALLOWED_PARAMS_EE = [ ALLOWED_PARAMS_EE = [
:issues_enabled,
:jenkins_url, :jenkins_url,
:multiproject_enabled, :multiproject_enabled,
:pass_unstable, :pass_unstable,
......
...@@ -9,6 +9,21 @@ module EE ...@@ -9,6 +9,21 @@ module EE
::Feature.enabled?(:jira_integration, @project) && @project.jira_service.issues_enabled ::Feature.enabled?(:jira_integration, @project) && @project.jira_service.issues_enabled
end end
override :integration_form_data
def integration_form_data(integration)
form_data = super
if integration.is_a?(JiraService)
form_data.merge!(
enable_jira_issues: integration.issues_enabled.to_s,
project_key: integration.project_key,
edit_project_path: @project ? edit_project_path(@project, anchor: 'js-shared-permissions') : nil
)
end
form_data
end
def add_to_slack_link(project, slack_app_id) def add_to_slack_link(project, slack_app_id)
"https://slack.com/oauth/authorize?scope=commands&client_id=#{slack_app_id}&redirect_uri=#{slack_auth_project_settings_slack_url(project)}&state=#{escaped_form_authenticity_token}" "https://slack.com/oauth/authorize?scope=commands&client_id=#{slack_app_id}&redirect_uri=#{slack_auth_project_settings_slack_url(project)}&state=#{escaped_form_authenticity_token}"
end end
......
...@@ -17,6 +17,26 @@ RSpec.describe EE::ServicesHelper do ...@@ -17,6 +17,26 @@ RSpec.describe EE::ServicesHelper do
subject { controller_class.new } subject { controller_class.new }
describe '#integration_form_data' do
subject { helper.integration_form_data(integration) }
context 'Slack service' do
let(:integration) { build(:slack_service) }
it 'does not include Jira specific fields' do
is_expected.not_to include(:enable_jira_issues, :project_key, :edit_project_path)
end
end
context 'Jira service' do
let(:integration) { build(:jira_service) }
it 'includes Jira specific fields' do
is_expected.to include(:enable_jira_issues, :project_key, :edit_project_path)
end
end
end
describe '#add_to_slack_link' do describe '#add_to_slack_link' do
it 'encodes a masked CSRF token' do it 'encodes a masked CSRF token' do
expect(subject).to receive(:form_authenticity_token).and_return('a token') expect(subject).to receive(:form_authenticity_token).and_return('a token')
......
...@@ -12943,6 +12943,12 @@ msgstr "" ...@@ -12943,6 +12943,12 @@ msgstr ""
msgid "JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}" msgid "JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}"
msgstr "" msgstr ""
msgid "JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used."
msgstr ""
msgid "JiraService|Enable Jira issues"
msgstr ""
msgid "JiraService|Events for %{noteable_model_name} are disabled." msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr "" msgstr ""
...@@ -12967,6 +12973,9 @@ msgstr "" ...@@ -12967,6 +12973,9 @@ msgstr ""
msgid "JiraService|Jira issue tracker" msgid "JiraService|Jira issue tracker"
msgstr "" msgstr ""
msgid "JiraService|Jira project key"
msgstr ""
msgid "JiraService|Open Jira" msgid "JiraService|Open Jira"
msgstr "" msgstr ""
...@@ -12988,9 +12997,21 @@ msgstr "" ...@@ -12988,9 +12997,21 @@ msgstr ""
msgid "JiraService|Username or Email" msgid "JiraService|Username or Email"
msgstr "" msgstr ""
msgid "JiraService|View Jira issues in GitLab"
msgstr ""
msgid "JiraService|Warning: All GitLab users that have access to this GitLab project will be able to view all issues from the Jira project specified below."
msgstr ""
msgid "JiraService|Web URL" msgid "JiraService|Web URL"
msgstr "" msgstr ""
msgid "JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of issues and view any issue as read-only."
msgstr ""
msgid "JiraService|e.g. AB"
msgstr ""
msgid "JiraService|transition ids can have only numbers which can be split with , or ;" msgid "JiraService|transition ids can have only numbers which can be split with , or ;"
msgstr "" msgstr ""
......
...@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue'; import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue'; import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue'; import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue'; import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue'; import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
...@@ -18,16 +19,20 @@ describe('IntegrationForm', () => { ...@@ -18,16 +19,20 @@ describe('IntegrationForm', () => {
initialTriggerMergeRequest: false, initialTriggerMergeRequest: false,
initialEnableComments: false, initialEnableComments: false,
}, },
jiraIssuesProps: {},
type: '', type: '',
}; };
const createComponent = props => { const createComponent = (props, featureFlags = {}) => {
wrapper = shallowMount(IntegrationForm, { wrapper = shallowMount(IntegrationForm, {
propsData: { ...defaultProps, ...props }, propsData: { ...defaultProps, ...props },
stubs: { stubs: {
ActiveToggle, ActiveToggle,
JiraTriggerFields, JiraTriggerFields,
}, },
provide: {
glFeatures: featureFlags,
},
}); });
}; };
...@@ -40,6 +45,7 @@ describe('IntegrationForm', () => { ...@@ -40,6 +45,7 @@ describe('IntegrationForm', () => {
const findActiveToggle = () => wrapper.find(ActiveToggle); const findActiveToggle = () => wrapper.find(ActiveToggle);
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields); const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
const findTriggerFields = () => wrapper.find(TriggerFields); const findTriggerFields = () => wrapper.find(TriggerFields);
describe('template', () => { describe('template', () => {
...@@ -62,23 +68,41 @@ describe('IntegrationForm', () => { ...@@ -62,23 +68,41 @@ describe('IntegrationForm', () => {
}); });
describe('type is "slack"', () => { describe('type is "slack"', () => {
it('does not render JiraTriggerFields', () => { beforeEach(() => {
createComponent({ createComponent({ type: 'slack' });
type: 'slack',
}); });
it('does not render JiraTriggerFields', () => {
expect(findJiraTriggerFields().exists()).toBe(false); expect(findJiraTriggerFields().exists()).toBe(false);
}); });
it('does not render JiraIssuesFields', () => {
expect(findJiraIssuesFields().exists()).toBe(false);
});
}); });
describe('type is "jira"', () => { describe('type is "jira"', () => {
it('renders JiraTriggerFields', () => { it('renders JiraTriggerFields', () => {
createComponent({ createComponent({ type: 'jira' });
type: 'jira',
});
expect(findJiraTriggerFields().exists()).toBe(true); expect(findJiraTriggerFields().exists()).toBe(true);
}); });
describe('featureFlag jiraIntegration is false', () => {
it('does not render JiraIssuesFields', () => {
createComponent({ type: 'jira' }, { jiraIntegration: false });
expect(findJiraIssuesFields().exists()).toBe(false);
});
});
describe('featureFlag jiraIntegration is true', () => {
it('renders JiraIssuesFields', () => {
createComponent({ type: 'jira' }, { jiraIntegration: true });
expect(findJiraIssuesFields().exists()).toBe(true);
});
});
}); });
describe('triggerEvents is present', () => { describe('triggerEvents is present', () => {
......
import { mount } from '@vue/test-utils';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
describe('JiraIssuesFields', () => {
let wrapper;
const defaultProps = {
editProjectPath: '/edit',
};
const createComponent = props => {
wrapper = mount(JiraIssuesFields, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const findEnableCheckbox = () => wrapper.find(GlFormCheckbox);
const findProjectKey = () => wrapper.find(GlFormInput);
describe('template', () => {
describe('Enable Jira issues checkbox', () => {
beforeEach(() => {
createComponent({ initialProjectKey: '' });
});
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
// browsers don't include unchecked boxes in form submissions.
it('includes issues_enabled as false even if unchecked', () => {
expect(wrapper.contains('input[name="service[issues_enabled]"]')).toBe(true);
});
it('disables project_key input', () => {
expect(findProjectKey().attributes('disabled')).toBe('disabled');
});
describe('on enable issues', () => {
it('enables project_key input', () => {
findEnableCheckbox().vm.$emit('input', true);
return wrapper.vm.$nextTick().then(() => {
expect(findProjectKey().attributes('disabled')).toBeUndefined();
});
});
});
});
it('contains link to editProjectPath', () => {
createComponent();
expect(wrapper.contains(`a[href="${defaultProps.editProjectPath}"]`)).toBe(true);
});
});
});
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