Commit fa7dbdb8 authored by Tom Quirk's avatar Tom Quirk

Use gl-form in integration_form.vue

This commit moves the <form> element
for integrations from HAML to Vue.

No user-facing changes should
be expected
parent cd465319
<script> <script>
import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml, GlForm } from '@gitlab/ui';
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
integrationLevels, integrationLevels,
} from '~/integrations/constants'; } from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { refreshCurrentPage } from '~/lib/utils/url_utility';
import csrf from '~/lib/utils/csrf';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import { testIntegrationSettings } from '../api'; import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue'; import ActiveCheckbox from './active_checkbox.vue';
...@@ -35,6 +36,7 @@ export default { ...@@ -35,6 +36,7 @@ export default {
ConfirmationModal, ConfirmationModal,
ResetConfirmationModal, ResetConfirmationModal,
GlButton, GlButton,
GlForm,
}, },
directives: { directives: {
GlModal: GlModalDirective, GlModal: GlModalDirective,
...@@ -42,10 +44,6 @@ export default { ...@@ -42,10 +44,6 @@ export default {
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
props: { props: {
formSelector: {
type: String,
required: true,
},
helpHtml: { helpHtml: {
type: String, type: String,
required: false, required: false,
...@@ -84,16 +82,14 @@ export default { ...@@ -84,16 +82,14 @@ export default {
disableButtons() { disableButtons() {
return Boolean(this.isSaving || this.isResetting || this.isTesting); return Boolean(this.isSaving || this.isResetting || this.isTesting);
}, },
}, form() {
mounted() { return this.$refs.integrationForm.$el;
// this form element is defined in Haml },
this.form = document.querySelector(this.formSelector);
}, },
methods: { methods: {
...mapActions(['setOverride', 'fetchResetIntegration', 'requestJiraIssueTypes']), ...mapActions(['setOverride', 'fetchResetIntegration', 'requestJiraIssueTypes']),
onSaveClick() { onSaveClick() {
this.isSaving = true; this.isSaving = true;
if (this.integrationActive && !this.form.checkValidity()) { if (this.integrationActive && !this.form.checkValidity()) {
this.isSaving = false; this.isSaving = false;
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT); eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
...@@ -152,16 +148,6 @@ export default { ...@@ -152,16 +148,6 @@ export default {
}, },
onToggleIntegrationState(integrationActive) { onToggleIntegrationState(integrationActive) {
this.integrationActive = integrationActive; this.integrationActive = integrationActive;
if (!this.form) {
return;
}
// If integration will be active, enable form validation.
if (integrationActive) {
this.form.removeAttribute('novalidate');
} else {
this.form.setAttribute('novalidate', true);
}
}, },
}, },
helpHtmlConfig: { helpHtmlConfig: {
...@@ -169,11 +155,27 @@ export default { ...@@ -169,11 +155,27 @@ export default {
ADD_TAGS: ['use'], // to support icon SVGs ADD_TAGS: ['use'], // to support icon SVGs
FORBID_ATTR: [], // This is trusted input so we can override the default config to allow data-* attributes FORBID_ATTR: [], // This is trusted input so we can override the default config to allow data-* attributes
}, },
csrf,
}; };
</script> </script>
<template> <template>
<div class="gl-mb-3"> <gl-form
ref="integrationForm"
method="post"
class="gl-mb-3 gl-show-field-errors integration-settings-form"
:action="propsSource.formPath"
:novalidate="!integrationActive"
>
<input type="hidden" name="_method" value="put" />
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<input
type="hidden"
name="redirect_to"
:value="propsSource.redirectTo"
data-testid="redirect-to-field"
/>
<override-dropdown <override-dropdown
v-if="defaultState !== null" v-if="defaultState !== null"
:inherit-from-id="defaultState.id" :inherit-from-id="defaultState.id"
...@@ -282,5 +284,5 @@ export default { ...@@ -282,5 +284,5 @@ export default {
</div> </div>
</div> </div>
</div> </div>
</div> </gl-form>
</template> </template>
...@@ -28,9 +28,11 @@ function parseDatasetToProps(data) { ...@@ -28,9 +28,11 @@ function parseDatasetToProps(data) {
cancelPath, cancelPath,
testPath, testPath,
resetPath, resetPath,
formPath,
vulnerabilitiesIssuetype, vulnerabilitiesIssuetype,
jiraIssueTransitionAutomatic, jiraIssueTransitionAutomatic,
jiraIssueTransitionId, jiraIssueTransitionId,
redirectTo,
...booleanAttributes ...booleanAttributes
} = data; } = data;
const { const {
...@@ -57,6 +59,7 @@ function parseDatasetToProps(data) { ...@@ -57,6 +59,7 @@ function parseDatasetToProps(data) {
canTest, canTest,
testPath, testPath,
resetPath, resetPath,
formPath,
triggerFieldsProps: { triggerFieldsProps: {
initialTriggerCommit: commitEvents, initialTriggerCommit: commitEvents,
initialTriggerMergeRequest: mergeRequestEvents, initialTriggerMergeRequest: mergeRequestEvents,
...@@ -82,10 +85,11 @@ function parseDatasetToProps(data) { ...@@ -82,10 +85,11 @@ function parseDatasetToProps(data) {
inheritFromId: parseInt(inheritFromId, 10), inheritFromId: parseInt(inheritFromId, 10),
integrationLevel, integrationLevel,
id: parseInt(id, 10), id: parseInt(id, 10),
redirectTo,
}; };
} }
export default function initIntegrationSettingsForm(formSelector) { export default function initIntegrationSettingsForm() {
const customSettingsEl = document.querySelector('.js-vue-integration-settings'); const customSettingsEl = document.querySelector('.js-vue-integration-settings');
const defaultSettingsEl = document.querySelector('.js-vue-default-integration-settings'); const defaultSettingsEl = document.querySelector('.js-vue-default-integration-settings');
...@@ -115,7 +119,6 @@ export default function initIntegrationSettingsForm(formSelector) { ...@@ -115,7 +119,6 @@ export default function initIntegrationSettingsForm(formSelector) {
return createElement(IntegrationForm, { return createElement(IntegrationForm, {
props: { props: {
helpHtml, helpHtml,
formSelector,
}, },
}); });
}, },
......
import initIntegrationSettingsForm from '~/integrations/edit'; import initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
initIntegrationSettingsForm('.js-integration-settings-form'); initIntegrationSettingsForm();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring'; const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector); const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
......
import initIntegrationSettingsForm from '~/integrations/edit'; import initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
initIntegrationSettingsForm('.js-integration-settings-form'); initIntegrationSettingsForm();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring'; const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector); const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
......
...@@ -2,7 +2,7 @@ import initIntegrationSettingsForm from '~/integrations/edit'; ...@@ -2,7 +2,7 @@ import initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusAlerts from '~/prometheus_alerts'; import PrometheusAlerts from '~/prometheus_alerts';
import CustomMetrics from '~/prometheus_metrics/custom_metrics'; import CustomMetrics from '~/prometheus_metrics/custom_metrics';
initIntegrationSettingsForm('.js-integration-settings-form'); initIntegrationSettingsForm();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring'; const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector); const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
......
...@@ -90,7 +90,9 @@ module IntegrationsHelper ...@@ -90,7 +90,9 @@ module IntegrationsHelper
cancel_path: scoped_integrations_path(project: project, group: group), cancel_path: scoped_integrations_path(project: project, group: group),
can_test: integration.testable?.to_s, can_test: integration.testable?.to_s,
test_path: scoped_test_integration_path(integration, project: project, group: group), test_path: scoped_test_integration_path(integration, project: project, group: group),
reset_path: scoped_reset_integration_path(integration, group: group) reset_path: scoped_reset_integration_path(integration, group: group),
form_path: scoped_integration_path(integration, project: project, group: group),
redirect_to: request.referer
} }
if integration.is_a?(Integrations::Jira) if integration.is_a?(Integrations::Jira)
......
...@@ -6,10 +6,7 @@ ...@@ -6,10 +6,7 @@
- if integration.operating? - if integration.operating?
= sprite_icon('check', css_class: 'gl-text-green-500') = sprite_icon('check', css_class: 'gl-text-green-500')
= form_for(integration, as: :service, url: scoped_integration_path(integration, project: @project, group: @group), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => test_project_integration_path(@project, integration) } }) do |form| = render 'shared/service_settings', integration: integration
= render 'shared/service_settings', form: form, integration: integration
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referer }
- if lookup_context.template_exists?('show', "projects/services/#{integration.to_param}", true) - if lookup_context.template_exists?('show', "projects/services/#{integration.to_param}", true)
%hr %hr
= render "projects/services/#{integration.to_param}/show", integration: integration = render "projects/services/#{integration.to_param}/show", integration: integration
- integration = local_assigns.fetch(:integration) - integration = local_assigns.fetch(:integration)
= form_for integration, as: :service, url: scoped_integration_path(integration, group: @group), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => scoped_test_integration_path(integration, group: @group) } } do |form| = render 'shared/service_settings', integration: integration
= render 'shared/service_settings', form: form, integration: integration
...@@ -22,7 +22,7 @@ RSpec.describe 'Slack application' do ...@@ -22,7 +22,7 @@ RSpec.describe 'Slack application' do
it 'I can edit slack integration' do it 'I can edit slack integration' do
visit slack_application_form_path visit slack_application_form_path
within '.js-integration-settings-form' do within '.service-settings' do
click_link 'Edit' click_link 'Edit'
end end
...@@ -31,7 +31,7 @@ RSpec.describe 'Slack application' do ...@@ -31,7 +31,7 @@ RSpec.describe 'Slack application' do
expect(page).to have_content('The project alias was updated successfully') expect(page).to have_content('The project alias was updated successfully')
within '.js-integration-settings-form' do within '.service-settings' do
expect(page).to have_content('alias-edited') expect(page).to have_content('alias-edited')
end end
end end
......
import { GlForm } from '@gitlab/ui';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { setHTMLFixture } from 'helpers/fixtures'; import { setHTMLFixture } from 'helpers/fixtures';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue'; import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue'; import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
...@@ -35,7 +36,6 @@ describe('IntegrationForm', () => { ...@@ -35,7 +36,6 @@ describe('IntegrationForm', () => {
let wrapper; let wrapper;
let dispatch; let dispatch;
let mockAxios; let mockAxios;
let mockForm;
const createComponent = ({ const createComponent = ({
customStateProps = {}, customStateProps = {},
...@@ -49,7 +49,7 @@ describe('IntegrationForm', () => { ...@@ -49,7 +49,7 @@ describe('IntegrationForm', () => {
}); });
dispatch = jest.spyOn(store, 'dispatch').mockImplementation(); dispatch = jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMountExtended(IntegrationForm, { wrapper = mountExtended(IntegrationForm, {
propsData: { ...props, formSelector: '.test' }, propsData: { ...props, formSelector: '.test' },
provide: { provide: {
glFeatures: featureFlags, glFeatures: featureFlags,
...@@ -70,13 +70,6 @@ describe('IntegrationForm', () => { ...@@ -70,13 +70,6 @@ describe('IntegrationForm', () => {
}); });
}; };
const createForm = ({ isValid = true } = {}) => {
mockForm = document.createElement('form');
jest.spyOn(document, 'querySelector').mockReturnValue(mockForm);
jest.spyOn(mockForm, 'checkValidity').mockReturnValue(isValid);
jest.spyOn(mockForm, 'submit');
};
const findOverrideDropdown = () => wrapper.findComponent(OverrideDropdown); const findOverrideDropdown = () => wrapper.findComponent(OverrideDropdown);
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox); const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal); const findConfirmationModal = () => wrapper.findComponent(ConfirmationModal);
...@@ -88,6 +81,8 @@ describe('IntegrationForm', () => { ...@@ -88,6 +81,8 @@ describe('IntegrationForm', () => {
const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields); const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields); const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
const findTriggerFields = () => wrapper.findComponent(TriggerFields); const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
beforeEach(() => { beforeEach(() => {
mockAxios = new MockAdapter(axios); mockAxios = new MockAdapter(axios);
...@@ -341,6 +336,16 @@ describe('IntegrationForm', () => { ...@@ -341,6 +336,16 @@ describe('IntegrationForm', () => {
}); });
}); });
}); });
it('renders hidden fields', () => {
createComponent({
customStateProps: {
redirectTo: '/services',
},
});
expect(findRedirectToField().attributes('value')).toBe('/services');
});
}); });
describe('ActiveCheckbox', () => { describe('ActiveCheckbox', () => {
...@@ -362,13 +367,12 @@ describe('IntegrationForm', () => { ...@@ -362,13 +367,12 @@ describe('IntegrationForm', () => {
describe.each` describe.each`
formActive | novalidate formActive | novalidate
${true} | ${null} ${true} | ${undefined}
${false} | ${'true'} ${false} | ${'novalidate'}
`( `(
'when `toggle-integration-active` is emitted with $formActive', 'when `toggle-integration-active` is emitted with $formActive',
({ formActive, novalidate }) => { ({ formActive, novalidate }) => {
beforeEach(async () => { beforeEach(async () => {
createForm();
createComponent({ createComponent({
customStateProps: { customStateProps: {
showActive: true, showActive: true,
...@@ -380,7 +384,7 @@ describe('IntegrationForm', () => { ...@@ -380,7 +384,7 @@ describe('IntegrationForm', () => {
}); });
it(`sets noValidate to ${novalidate}`, () => { it(`sets noValidate to ${novalidate}`, () => {
expect(mockForm.getAttribute('novalidate')).toBe(novalidate); expect(findGlForm().attributes('novalidate')).toBe(novalidate);
}); });
}, },
); );
...@@ -389,7 +393,6 @@ describe('IntegrationForm', () => { ...@@ -389,7 +393,6 @@ describe('IntegrationForm', () => {
describe('when `save` button is clicked', () => { describe('when `save` button is clicked', () => {
describe('buttons', () => { describe('buttons', () => {
beforeEach(async () => { beforeEach(async () => {
createForm();
createComponent({ createComponent({
customStateProps: { customStateProps: {
showActive: true, showActive: true,
...@@ -419,7 +422,6 @@ describe('IntegrationForm', () => { ...@@ -419,7 +422,6 @@ describe('IntegrationForm', () => {
'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)', 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)',
({ integrationActive, checkValidityReturn }) => { ({ integrationActive, checkValidityReturn }) => {
beforeEach(async () => { beforeEach(async () => {
createForm({ isValid: checkValidityReturn });
createComponent({ createComponent({
customStateProps: { customStateProps: {
showActive: true, showActive: true,
...@@ -427,19 +429,20 @@ describe('IntegrationForm', () => { ...@@ -427,19 +429,20 @@ describe('IntegrationForm', () => {
initialActivated: integrationActive, initialActivated: integrationActive,
}, },
}); });
jest.spyOn(findGlForm().element, 'submit');
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn);
await findProjectSaveButton().vm.$emit('click', new Event('click')); await findProjectSaveButton().vm.$emit('click', new Event('click'));
}); });
it('submit form', () => { it('submit form', () => {
expect(mockForm.submit).toHaveBeenCalledTimes(1); expect(findGlForm().element.submit).toHaveBeenCalledTimes(1);
}); });
}, },
); );
describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => { describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => {
beforeEach(async () => { beforeEach(async () => {
createForm({ isValid: false });
createComponent({ createComponent({
customStateProps: { customStateProps: {
showActive: true, showActive: true,
...@@ -447,12 +450,14 @@ describe('IntegrationForm', () => { ...@@ -447,12 +450,14 @@ describe('IntegrationForm', () => {
initialActivated: true, initialActivated: true,
}, },
}); });
jest.spyOn(findGlForm().element, 'submit');
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
await findProjectSaveButton().vm.$emit('click', new Event('click')); await findProjectSaveButton().vm.$emit('click', new Event('click'));
}); });
it('does not submit form', () => { it('does not submit form', () => {
expect(mockForm.submit).not.toHaveBeenCalled(); expect(findGlForm().element.submit).not.toHaveBeenCalled();
}); });
it('sets save button `loading` prop to `false`', () => { it('sets save button `loading` prop to `false`', () => {
...@@ -472,13 +477,13 @@ describe('IntegrationForm', () => { ...@@ -472,13 +477,13 @@ describe('IntegrationForm', () => {
describe('when `test` button is clicked', () => { describe('when `test` button is clicked', () => {
describe('when form is invalid', () => { describe('when form is invalid', () => {
it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => { it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
createForm({ isValid: false });
createComponent({ createComponent({
customStateProps: { customStateProps: {
showActive: true, showActive: true,
canTest: true, canTest: true,
}, },
}); });
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false);
findTestButton().vm.$emit('click', new Event('click')); findTestButton().vm.$emit('click', new Event('click'));
...@@ -490,7 +495,6 @@ describe('IntegrationForm', () => { ...@@ -490,7 +495,6 @@ describe('IntegrationForm', () => {
const mockTestPath = '/test'; const mockTestPath = '/test';
beforeEach(() => { beforeEach(() => {
createForm({ isValid: true });
createComponent({ createComponent({
customStateProps: { customStateProps: {
showActive: true, showActive: true,
...@@ -498,6 +502,7 @@ describe('IntegrationForm', () => { ...@@ -498,6 +502,7 @@ describe('IntegrationForm', () => {
testPath: mockTestPath, testPath: mockTestPath,
}, },
}); });
jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(true);
}); });
describe('buttons', () => { describe('buttons', () => {
......
...@@ -20,6 +20,12 @@ RSpec.describe IntegrationsHelper do ...@@ -20,6 +20,12 @@ RSpec.describe IntegrationsHelper do
end end
describe '#integration_form_data' do describe '#integration_form_data' do
before do
allow(helper).to receive_messages(
request: double(referer: '/services')
)
end
let(:fields) do let(:fields) do
[ [
:id, :id,
...@@ -39,7 +45,8 @@ RSpec.describe IntegrationsHelper do ...@@ -39,7 +45,8 @@ RSpec.describe IntegrationsHelper do
:cancel_path, :cancel_path,
:can_test, :can_test,
:test_path, :test_path,
:reset_path :reset_path,
:redirect_to
] ]
end end
...@@ -61,6 +68,10 @@ RSpec.describe IntegrationsHelper do ...@@ -61,6 +68,10 @@ RSpec.describe IntegrationsHelper do
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
specify do
expect(subject[:redirect_to]).to eq('/services')
end
end end
context 'Jira service' do context 'Jira service' do
......
...@@ -23,10 +23,6 @@ RSpec.describe 'projects/services/_form' do ...@@ -23,10 +23,6 @@ RSpec.describe 'projects/services/_form' do
context 'commit_events and merge_request_events' do context 'commit_events and merge_request_events' do
it 'display merge_request_events and commit_events descriptions' do it 'display merge_request_events and commit_events descriptions' do
allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request)) allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request))
render
expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
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