Commit 2a036b35 authored by Tom Quirk's avatar Tom Quirk

Remove event hub from integrations app

This commit removes the event hub,
and refactors the integration form
to pass validated state down to child components
from parent.

No user facing changes are expected.
parent bbf52b48
import { s__, __ } from '~/locale';
export const VALIDATE_INTEGRATION_FORM_EVENT = 'validateIntegrationForm';
export const integrationLevels = {
GROUP: 'group',
INSTANCE: 'instance',
......
......@@ -9,8 +9,6 @@ import {
} from '@gitlab/ui';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { mapGetters } from 'vuex';
import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import eventHub from '../event_hub';
export default {
name: 'DynamicField',
......@@ -70,11 +68,15 @@ export default {
required: false,
default: null,
},
isValidated: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
model: this.value,
validated: false,
};
},
computed: {
......@@ -123,22 +125,13 @@ export default {
};
},
valid() {
return !this.required || !isEmpty(this.model) || this.isNonEmptyPassword || !this.validated;
return !this.required || !isEmpty(this.model) || this.isNonEmptyPassword || !this.isValidated;
},
},
created() {
if (this.isNonEmptyPassword) {
this.model = null;
}
eventHub.$on(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
},
beforeDestroy() {
eventHub.$off(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
},
methods: {
validateForm() {
this.validated = true;
},
},
helpHtmlConfig: {
ADD_ATTR: ['target'], // allow external links, can be removed after https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1427 is implemented
......
......@@ -5,7 +5,6 @@ import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
VALIDATE_INTEGRATION_FORM_EVENT,
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
......@@ -14,7 +13,6 @@ import {
} from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import csrf from '~/lib/utils/csrf';
import eventHub from '../event_hub';
import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
import ConfirmationModal from './confirmation_modal.vue';
......@@ -57,6 +55,7 @@ export default {
isTesting: false,
isSaving: false,
isResetting: false,
isValidated: false,
};
},
computed: {
......@@ -107,13 +106,16 @@ export default {
: document.querySelector(INTEGRATION_FORM_SELECTOR);
},
methods: {
...mapActions(['setOverride', 'fetchResetIntegration', 'requestJiraIssueTypes']),
...mapActions(['setOverride', 'requestJiraIssueTypes']),
setIsValidated() {
this.isValidated = true;
},
onSaveClick() {
this.isSaving = true;
if (this.integrationActive && !this.form.checkValidity()) {
this.isSaving = false;
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
this.setIsValidated();
return;
}
......@@ -123,14 +125,14 @@ export default {
this.isTesting = true;
if (!this.form.checkValidity()) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
this.setIsValidated();
return;
}
testIntegrationSettings(this.propsSource.testPath, this.getFormData())
.then(({ data: { error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } }) => {
if (error) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
this.setIsValidated();
this.$toast.show(message);
return;
}
......@@ -227,6 +229,7 @@ export default {
v-if="isJira"
:key="`${currentKey}-jira-trigger-fields`"
v-bind="propsSource.triggerFieldsProps"
:is-validated="isValidated"
/>
<trigger-fields
v-else-if="propsSource.triggerEvents.length"
......@@ -238,11 +241,13 @@ export default {
v-for="field in propsSource.fields"
:key="`${currentKey}-${field.name}`"
v-bind="field"
:is-validated="isValidated"
/>
<jira-issues-fields
v-if="isJira && !isInstanceOrGroupLevel"
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
:is-validated="isValidated"
@request-jira-issue-types="onRequestJiraIssueTypes"
/>
......
<script>
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import { s__, __ } from '~/locale';
import eventHub from '../event_hub';
import JiraUpgradeCta from './jira_upgrade_cta.vue';
export default {
......@@ -64,29 +62,22 @@ export default {
required: false,
default: '',
},
isValidated: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
enableJiraIssues: this.initialEnableJiraIssues,
projectKey: this.initialProjectKey,
validated: false,
};
},
computed: {
...mapGetters(['isInheriting']),
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
},
created() {
eventHub.$on(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
},
beforeDestroy() {
eventHub.$off(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
},
methods: {
validateForm() {
this.validated = true;
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.isValidated;
},
},
i18n: {
......
......@@ -9,9 +9,7 @@ import {
} from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
const commentDetailOptions = [
{
......@@ -92,10 +90,14 @@ export default {
required: false,
default: '',
},
isValidated: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
validated: false,
triggerCommit: this.initialTriggerCommit,
triggerMergeRequest: this.initialTriggerMergeRequest,
enableComments: this.initialEnableComments,
......@@ -115,19 +117,10 @@ export default {
return this.triggerCommit || this.triggerMergeRequest;
},
validIssueTransitionId() {
return !this.validated || Boolean(this.jiraIssueTransitionId);
return !this.isValidated || Boolean(this.jiraIssueTransitionId);
},
},
created() {
eventHub.$on(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
},
beforeDestroy() {
eventHub.$off(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
},
methods: {
validateForm() {
this.validated = true;
},
showCustomIssueTransitions(currentOption) {
return (
this.jiraIssueTransitionAutomatic === ISSUE_TRANSITION_CUSTOM &&
......
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
import {
VALIDATE_INTEGRATION_FORM_EVENT,
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
} from '~/integrations/constants';
import { testIntegrationSettings } from '../api';
import eventHub from '../event_hub';
import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
......@@ -19,7 +17,6 @@ export const requestJiraIssueTypes = ({ commit, dispatch, getters }, formData) =
data: { issuetypes, error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE },
}) => {
if (error || !issuetypes?.length) {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
throw new Error(message);
}
......
export const SET_OVERRIDE = 'SET_OVERRIDE';
export const SET_IS_RESETTING = 'SET_IS_RESETTING';
export const SET_IS_LOADING_JIRA_ISSUE_TYPES = 'SET_IS_LOADING_JIRA_ISSUE_TYPES';
export const SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE = 'SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE';
export const SET_JIRA_ISSUE_TYPES = 'SET_JIRA_ISSUE_TYPES';
export const REQUEST_RESET_INTEGRATION = 'REQUEST_RESET_INTEGRATION';
export const RECEIVE_RESET_INTEGRATION_ERROR = 'RECEIVE_RESET_INTEGRATION_ERROR';
......@@ -2,22 +2,14 @@ import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea
import { mount } from '@vue/test-utils';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import { mockField } from '../mock_data';
describe('DynamicField', () => {
let wrapper;
const defaultProps = {
help: 'The URL of the project',
name: 'project_url',
placeholder: 'https://jira.example.com',
title: 'Project URL',
type: 'text',
value: '1',
};
const createComponent = (props, isInheriting = false) => {
wrapper = mount(DynamicField, {
propsData: { ...defaultProps, ...props },
propsData: { ...mockField, ...props },
computed: {
isInheriting: () => isInheriting,
},
......@@ -61,7 +53,7 @@ describe('DynamicField', () => {
});
it(`renders GlFormCheckbox with correct text content when checkboxLabel is ${checkboxLabel}`, () => {
expect(findGlFormCheckbox().text()).toContain(checkboxLabel ?? defaultProps.title);
expect(findGlFormCheckbox().text()).toContain(checkboxLabel ?? mockField.title);
});
it('does not render other types of input', () => {
......@@ -160,7 +152,7 @@ describe('DynamicField', () => {
type: 'text',
id: 'service_project_url',
name: 'service[project_url]',
placeholder: defaultProps.placeholder,
placeholder: mockField.placeholder,
required: 'required',
});
expect(findGlFormInput().attributes('readonly')).toBe(readonly);
......@@ -179,7 +171,7 @@ describe('DynamicField', () => {
it('renders description with help text', () => {
createComponent();
expect(findGlFormGroup().find('small').text()).toBe(defaultProps.help);
expect(findGlFormGroup().find('small').text()).toBe(mockField.help);
});
describe('when type is checkbox', () => {
......@@ -189,7 +181,7 @@ describe('DynamicField', () => {
});
expect(findGlFormGroup().find('small').exists()).toBe(false);
expect(findGlFormCheckbox().text()).toContain(defaultProps.help);
expect(findGlFormCheckbox().text()).toContain(mockField.help);
});
});
......@@ -221,40 +213,36 @@ describe('DynamicField', () => {
it('renders label with title', () => {
createComponent();
expect(findGlFormGroup().find('label').text()).toBe(defaultProps.title);
expect(findGlFormGroup().find('label').text()).toBe(mockField.title);
});
});
describe('validations', () => {
describe('password field', () => {
beforeEach(() => {
describe('password field validations', () => {
describe('without value', () => {
it('requires validation', () => {
createComponent({
type: 'password',
required: true,
value: null,
isValidated: true,
});
wrapper.vm.validated = true;
});
describe('without value', () => {
it('requires validation', () => {
expect(wrapper.vm.valid).toBe(false);
expect(findGlFormGroup().classes('is-invalid')).toBe(true);
expect(findGlFormInput().classes('is-invalid')).toBe(true);
});
expect(findGlFormGroup().classes('is-invalid')).toBe(true);
expect(findGlFormInput().classes('is-invalid')).toBe(true);
});
});
describe('with value', () => {
beforeEach(() => {
wrapper.setProps({ value: 'true' });
describe('with value', () => {
it('does not require validation', () => {
createComponent({
type: 'password',
required: true,
value: 'test value',
isValidated: true,
});
it('does not require validation', () => {
expect(wrapper.vm.valid).toBe(true);
expect(findGlFormGroup().classes('is-valid')).toBe(true);
expect(findGlFormInput().classes('is-valid')).toBe(true);
});
expect(findGlFormGroup().classes('is-valid')).toBe(true);
expect(findGlFormInput().classes('is-valid')).toBe(true);
});
});
});
......
......@@ -17,16 +17,13 @@ import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import {
integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
VALIDATE_INTEGRATION_FORM_EVENT,
I18N_DEFAULT_ERROR_MESSAGE,
} from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
import eventHub from '~/integrations/edit/event_hub';
import httpStatus from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { mockIntegrationProps } from '../mock_data';
import { mockIntegrationProps, mockField } from '../mock_data';
jest.mock('~/integrations/edit/event_hub');
jest.mock('@sentry/browser');
jest.mock('~/lib/utils/url_utility');
......@@ -97,6 +94,7 @@ describe('IntegrationForm', () => {
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
const findFormElement = () => (vueIntegrationFormFeatureFlag ? findGlForm().element : mockForm);
const findDynamicField = () => wrapper.findComponent(DynamicField);
const mockFormFunctions = ({ checkValidityReturn }) => {
jest.spyOn(findFormElement(), 'checkValidity').mockReturnValue(checkValidityReturn);
......@@ -490,6 +488,7 @@ describe('IntegrationForm', () => {
showActive: true,
canTest: true,
initialActivated: true,
fields: [mockField],
},
mountFn: mountExtended,
});
......@@ -510,27 +509,28 @@ describe('IntegrationForm', () => {
expect(findTestButton().props('disabled')).toBe(false);
});
it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => {
expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
it('sets `isValidated` props on form fields', () => {
expect(findDynamicField().props('isValidated')).toBe(true);
});
});
});
describe('when `test` button is clicked', () => {
describe('when form is invalid', () => {
it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
it('sets `isValidated` props on form fields', async () => {
createComponent({
customStateProps: {
showActive: true,
canTest: true,
fields: [mockField],
},
mountFn: mountExtended,
});
mockFormFunctions({ checkValidityReturn: false });
findTestButton().vm.$emit('click', new Event('click'));
await findTestButton().vm.$emit('click', new Event('click'));
expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
expect(findDynamicField().props('isValidated')).toBe(true);
});
});
......
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store';
describe('JiraIssuesFields', () => {
......@@ -222,7 +220,7 @@ describe('JiraIssuesFields', () => {
});
describe('Project key input field', () => {
beforeEach(() => {
it('sets Project Key `state` attribute to `true` by default', () => {
createComponent({
props: {
initialProjectKey: '',
......@@ -230,29 +228,32 @@ describe('JiraIssuesFields', () => {
},
mountFn: shallowMountExtended,
});
});
it('sets Project Key `state` attribute to `true` by default', () => {
assertProjectKeyState('true');
});
describe('when event hub recieves `VALIDATE_INTEGRATION_FORM_EVENT` event', () => {
describe('when `isValidated` prop is true', () => {
beforeEach(() => {
createComponent({
props: {
initialProjectKey: '',
initialEnableJiraIssues: true,
isValidated: true,
},
mountFn: shallowMountExtended,
});
});
describe('with no project key', () => {
it('sets Project Key `state` attribute to `undefined`', async () => {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
await wrapper.vm.$nextTick();
assertProjectKeyState(undefined);
});
});
describe('when project key is set', () => {
it('sets Project Key `state` attribute to `true`', async () => {
eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
// set the project key
await findProjectKey().vm.$emit('input', 'AB');
await wrapper.vm.$nextTick();
assertProjectKeyState('true');
});
......
......@@ -20,3 +20,12 @@ export const mockJiraIssueTypes = [
{ id: '2', name: 'bug', description: 'bug' },
{ id: '3', name: 'epic', description: 'epic' },
];
export const mockField = {
help: 'The URL of the project',
name: 'project_url',
placeholder: 'https://jira.example.com',
title: 'Project URL',
type: 'text',
value: '1',
};
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