Commit 52642981 authored by Peter Hegman's avatar Peter Hegman

Merge branch '332280-integration-settings-template-frontend-connection-details' into 'master'

Add frontend for connection section

See merge request gitlab-org/gitlab!82005
parents 4db3053f 58314d40
...@@ -25,3 +25,11 @@ export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection s ...@@ -25,3 +25,11 @@ export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection s
export const settingsTabTitle = __('Settings'); export const settingsTabTitle = __('Settings');
export const overridesTabTitle = s__('Integrations|Projects using custom settings'); export const overridesTabTitle = s__('Integrations|Projects using custom settings');
export const integrationFormSections = {
CONNECTION: 'connection',
};
export const integrationFormSectionComponents = {
[integrationFormSections.CONNECTION]: 'IntegrationSectionConnection',
};
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
I18N_DEFAULT_ERROR_MESSAGE, I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE, I18N_SUCCESSFUL_CONNECTION_MESSAGE,
integrationLevels, integrationLevels,
integrationFormSectionComponents,
} 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 csrf from '~/lib/utils/csrf';
...@@ -34,6 +35,10 @@ export default { ...@@ -34,6 +35,10 @@ export default {
DynamicField, DynamicField,
ConfirmationModal, ConfirmationModal,
ResetConfirmationModal, ResetConfirmationModal,
IntegrationSectionConnection: () =>
import(
/* webpackChunkName: 'integrationSectionConnection' */ '~/integrations/edit/components/sections/connection.vue'
),
GlButton, GlButton,
GlForm, GlForm,
}, },
...@@ -80,9 +85,23 @@ export default { ...@@ -80,9 +85,23 @@ export default {
disableButtons() { disableButtons() {
return Boolean(this.isSaving || this.isResetting || this.isTesting); return Boolean(this.isSaving || this.isResetting || this.isTesting);
}, },
sectionsEnabled() {
return this.glFeatures.integrationFormSections;
},
hasSections() {
return this.sectionsEnabled && this.customState.sections.length !== 0;
},
fieldsWithoutSection() {
return this.sectionsEnabled
? this.propsSource.fields.filter((field) => !field.section)
: this.propsSource.fields;
},
}, },
methods: { methods: {
...mapActions(['setOverride', 'requestJiraIssueTypes']), ...mapActions(['setOverride', 'requestJiraIssueTypes']),
fieldsForSection(section) {
return this.propsSource.fields.filter((field) => field.section === section.type);
},
form() { form() {
return this.$refs.integrationForm.$el; return this.$refs.integrationForm.$el;
}, },
...@@ -158,6 +177,7 @@ export default { ...@@ -158,6 +177,7 @@ export default {
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, csrf,
integrationFormSectionComponents,
}; };
</script> </script>
...@@ -186,15 +206,40 @@ export default { ...@@ -186,15 +206,40 @@ export default {
@change="setOverride" @change="setOverride"
/> />
<template v-if="hasSections">
<div
v-for="section in customState.sections"
:key="section.type"
class="gl-border-b gl-mb-5"
data-testid="integration-section"
>
<div class="row">
<div class="col-lg-4">
<h4 class="gl-mt-0">{{ section.title }}</h4>
<p v-safe-html="section.description"></p>
</div>
<div class="col-lg-8">
<component
:is="$options.integrationFormSectionComponents[section.type]"
:fields="fieldsForSection(section)"
:is-validated="isValidated"
@toggle-integration-active="onToggleIntegrationState"
/>
</div>
</div>
</div>
</template>
<div class="row"> <div class="row">
<div class="col-lg-4"></div> <div class="col-lg-4"></div>
<div class="col-lg-8"> <div class="col-lg-8">
<!-- helpHtml is trusted input --> <!-- helpHtml is trusted input -->
<div v-if="helpHtml" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div> <div v-if="helpHtml && !hasSections" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div>
<active-checkbox <active-checkbox
v-if="propsSource.showActive" v-if="propsSource.showActive && !hasSections"
:key="`${currentKey}-active-checkbox`" :key="`${currentKey}-active-checkbox`"
@toggle-integration-active="onToggleIntegrationState" @toggle-integration-active="onToggleIntegrationState"
/> />
...@@ -211,7 +256,7 @@ export default { ...@@ -211,7 +256,7 @@ export default {
:type="propsSource.type" :type="propsSource.type"
/> />
<dynamic-field <dynamic-field
v-for="field in propsSource.fields" v-for="field in fieldsWithoutSection"
:key="`${currentKey}-${field.name}`" :key="`${currentKey}-${field.name}`"
v-bind="field" v-bind="field"
:is-validated="isValidated" :is-validated="isValidated"
......
<script>
import { mapGetters } from 'vuex';
import ActiveCheckbox from '../active_checkbox.vue';
import DynamicField from '../dynamic_field.vue';
export default {
name: 'IntegrationSectionConnection',
components: {
ActiveCheckbox,
DynamicField,
},
props: {
fields: {
type: Array,
required: false,
default: () => [],
},
isValidated: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapGetters(['currentKey', 'propsSource']),
},
};
</script>
<template>
<div>
<active-checkbox
v-if="propsSource.showActive"
:key="`${currentKey}-active-checkbox`"
@toggle-integration-active="$emit('toggle-integration-active', $event)"
/>
<dynamic-field
v-for="field in fields"
:key="`${currentKey}-${field.name}`"
v-bind="field"
:is-validated="isValidated"
/>
</div>
</template>
...@@ -14,6 +14,8 @@ import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_field ...@@ -14,6 +14,8 @@ import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_field
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue'; import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue'; import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue'; import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import IntegrationSectionConnection from '~/integrations/edit/components/sections/connection.vue';
import { import {
integrationLevels, integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE, I18N_SUCCESSFUL_CONNECTION_MESSAGE,
...@@ -22,7 +24,7 @@ import { ...@@ -22,7 +24,7 @@ import {
import { createStore } from '~/integrations/edit/store'; import { createStore } from '~/integrations/edit/store';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { mockIntegrationProps, mockField } from '../mock_data'; import { mockIntegrationProps, mockField, mockSectionConnection } from '../mock_data';
jest.mock('@sentry/browser'); jest.mock('@sentry/browser');
jest.mock('~/lib/utils/url_utility'); jest.mock('~/lib/utils/url_utility');
...@@ -78,6 +80,11 @@ describe('IntegrationForm', () => { ...@@ -78,6 +80,11 @@ describe('IntegrationForm', () => {
const findGlForm = () => wrapper.findComponent(GlForm); const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field'); const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
const findDynamicField = () => wrapper.findComponent(DynamicField); const findDynamicField = () => wrapper.findComponent(DynamicField);
const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
const findAllSections = () => wrapper.findAllByTestId('integration-section');
const findConnectionSection = () => findAllSections().at(0);
const findConnectionSectionComponent = () =>
findConnectionSection().findComponent(IntegrationSectionConnection);
beforeEach(() => { beforeEach(() => {
mockAxios = new MockAdapter(axios); mockAxios = new MockAdapter(axios);
...@@ -253,23 +260,32 @@ describe('IntegrationForm', () => { ...@@ -253,23 +260,32 @@ describe('IntegrationForm', () => {
}); });
describe('fields is present', () => { describe('fields is present', () => {
it('renders DynamicField for each field', () => { it('renders DynamicField for each field without a section', () => {
const fields = [ const sectionFields = [
{ name: 'username', type: 'text' }, { name: 'username', type: 'text', section: mockSectionConnection.type },
{ name: 'API token', type: 'password' }, { name: 'API token', type: 'password', section: mockSectionConnection.type },
];
const nonSectionFields = [
{ name: 'branch', type: 'text' },
{ name: 'labels', type: 'select' },
]; ];
createComponent({ createComponent({
provide: {
glFeatures: { integrationFormSections: true },
},
customStateProps: { customStateProps: {
fields, sections: [mockSectionConnection],
fields: [...sectionFields, ...nonSectionFields],
}, },
}); });
const dynamicFields = wrapper.findAll(DynamicField); const dynamicFields = findAllDynamicFields();
expect(dynamicFields).toHaveLength(2); expect(dynamicFields).toHaveLength(2);
dynamicFields.wrappers.forEach((field, index) => { dynamicFields.wrappers.forEach((field, index) => {
expect(field.props()).toMatchObject(fields[index]); expect(field.props()).toMatchObject(nonSectionFields[index]);
}); });
}); });
}); });
...@@ -344,6 +360,83 @@ describe('IntegrationForm', () => { ...@@ -344,6 +360,83 @@ describe('IntegrationForm', () => {
}); });
}); });
describe('when integration has sections', () => {
beforeEach(() => {
createComponent({
provide: {
glFeatures: { integrationFormSections: true },
},
customStateProps: {
sections: [mockSectionConnection],
},
});
});
it('renders the expected number of sections', () => {
expect(findAllSections().length).toBe(1);
});
it('renders title, description and the correct dynamic component', () => {
const connectionSection = findConnectionSection();
expect(connectionSection.find('h4').text()).toBe(mockSectionConnection.title);
expect(connectionSection.find('p').text()).toBe(mockSectionConnection.description);
expect(findConnectionSectionComponent().exists()).toBe(true);
});
it('passes only fields with section type', () => {
const sectionFields = [
{ name: 'username', type: 'text', section: mockSectionConnection.type },
{ name: 'API token', type: 'password', section: mockSectionConnection.type },
];
const nonSectionFields = [
{ name: 'branch', type: 'text' },
{ name: 'labels', type: 'select' },
];
createComponent({
provide: {
glFeatures: { integrationFormSections: true },
},
customStateProps: {
sections: [mockSectionConnection],
fields: [...sectionFields, ...nonSectionFields],
},
});
expect(findConnectionSectionComponent().props('fields')).toEqual(sectionFields);
});
describe.each`
formActive | novalidate
${true} | ${undefined}
${false} | ${'true'}
`(
'when `toggle-integration-active` is emitted with $formActive',
({ formActive, novalidate }) => {
beforeEach(() => {
createComponent({
provide: {
glFeatures: { integrationFormSections: true },
},
customStateProps: {
sections: [mockSectionConnection],
showActive: true,
initialActivated: false,
},
});
findConnectionSectionComponent().vm.$emit('toggle-integration-active', formActive);
});
it(`sets noValidate to ${novalidate}`, () => {
expect(findGlForm().attributes('novalidate')).toBe(novalidate);
});
},
);
});
describe('ActiveCheckbox', () => { describe('ActiveCheckbox', () => {
describe.each` describe.each`
showActive showActive
...@@ -368,7 +461,7 @@ describe('IntegrationForm', () => { ...@@ -368,7 +461,7 @@ describe('IntegrationForm', () => {
`( `(
'when `toggle-integration-active` is emitted with $formActive', 'when `toggle-integration-active` is emitted with $formActive',
({ formActive, novalidate }) => { ({ formActive, novalidate }) => {
beforeEach(async () => { beforeEach(() => {
createComponent({ createComponent({
customStateProps: { customStateProps: {
showActive: true, showActive: true,
...@@ -376,7 +469,7 @@ describe('IntegrationForm', () => { ...@@ -376,7 +469,7 @@ describe('IntegrationForm', () => {
}, },
}); });
await findActiveCheckbox().vm.$emit('toggle-integration-active', formActive); findActiveCheckbox().vm.$emit('toggle-integration-active', formActive);
}); });
it(`sets noValidate to ${novalidate}`, () => { it(`sets noValidate to ${novalidate}`, () => {
......
import { shallowMount } from '@vue/test-utils';
import IntegrationSectionConnection from '~/integrations/edit/components/sections/connection.vue';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import { createStore } from '~/integrations/edit/store';
import { mockIntegrationProps } from '../../mock_data';
describe('IntegrationSectionConnection', () => {
let wrapper;
const createComponent = ({ customStateProps = {}, props = {} } = {}) => {
const store = createStore({
customState: { ...mockIntegrationProps, ...customStateProps },
});
wrapper = shallowMount(IntegrationSectionConnection, {
propsData: { ...props },
store,
});
};
afterEach(() => {
wrapper.destroy();
});
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
describe('template', () => {
describe('ActiveCheckbox', () => {
describe.each`
showActive
${true}
${false}
`('when `showActive` is $showActive', ({ showActive }) => {
it(`${showActive ? 'renders' : 'does not render'} ActiveCheckbox`, () => {
createComponent({
customStateProps: {
showActive,
},
});
expect(findActiveCheckbox().exists()).toBe(showActive);
});
});
});
describe('DynamicField', () => {
it('renders DynamicField for each field', () => {
const fields = [
{ name: 'username', type: 'text' },
{ name: 'API token', type: 'password' },
];
createComponent({
props: {
fields,
},
});
const dynamicFields = findAllDynamicFields();
expect(dynamicFields).toHaveLength(2);
dynamicFields.wrappers.forEach((field, index) => {
expect(field.props()).toMatchObject(fields[index]);
});
});
it('does not render DynamicField when field is empty', () => {
createComponent();
expect(findAllDynamicFields()).toHaveLength(0);
});
});
});
});
...@@ -10,6 +10,7 @@ export const mockIntegrationProps = { ...@@ -10,6 +10,7 @@ export const mockIntegrationProps = {
}, },
jiraIssuesProps: {}, jiraIssuesProps: {},
triggerEvents: [], triggerEvents: [],
sections: [],
fields: [], fields: [],
type: '', type: '',
inheritFromId: 25, inheritFromId: 25,
...@@ -30,3 +31,9 @@ export const mockField = { ...@@ -30,3 +31,9 @@ export const mockField = {
type: 'text', type: 'text',
value: '1', value: '1',
}; };
export const mockSectionConnection = {
type: 'connection',
title: 'Connection details',
description: 'Learn more on how to configure this integration.',
};
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