Commit 9f634d02 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '287827-create-shared-form' into 'master'

Create a shared form for compliance frameworks creation and updating

See merge request gitlab-org/gitlab!52475
parents 3bb5be27 3b4c53fc
<script>
import {
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlLoadingIcon,
GlSprintf,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { helpPagePath } from '~/helpers/help_page_helper';
import { validateHexColor } from '~/lib/utils/color_utils';
import { __, s__ } from '~/locale';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
const hasRequiredProperties = (value) => {
if (isEmpty(value)) {
return true;
}
return ['name', 'description', 'color'].every((prop) => value[prop]);
};
export default {
components: {
ColorPicker,
GlAlert,
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
GlLink,
GlLoadingIcon,
GlSprintf,
},
props: {
complianceFramework: {
type: Object,
required: false,
default: () => ({}),
validator: hasRequiredProperties,
},
error: {
type: String,
required: false,
default: null,
},
groupEditPath: {
type: String,
required: true,
},
loading: {
type: Boolean,
required: false,
default: false,
},
renderForm: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
name: null,
description: null,
color: null,
};
},
computed: {
isValidColor() {
return validateHexColor(this.color);
},
isValidName() {
if (this.name === null) {
return null;
}
return Boolean(this.name);
},
isValidDescription() {
if (this.description === null) {
return null;
}
return Boolean(this.description);
},
disableSubmitBtn() {
return !this.isValidName || !this.isValidDescription || !this.isValidColor;
},
scopedLabelsHelpPath() {
return helpPagePath('user/project/labels.md', { anchor: 'scoped-labels' });
},
},
watch: {
complianceFramework: {
handler() {
if (!isEmpty(this.complianceFramework)) {
this.name = this.complianceFramework.name;
this.description = this.complianceFramework.description;
this.color = this.complianceFramework.color;
}
},
immediate: true,
},
},
methods: {
onSubmit() {
const { name, description, color } = this;
this.$emit('submit', { name, description, color });
},
},
i18n: {
titleInputLabel: __('Title'),
titleInputDescription: s__(
'ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})',
),
titleInputInvalid: __('A title is required'),
descriptionInputLabel: __('Description'),
descriptionInputInvalid: __('A description is required'),
colorInputLabel: __('Background color'),
submitBtnText: __('Save changes'),
cancelBtnText: __('Cancel'),
},
};
</script>
<template>
<div class="gl-border-t-1 gl-border-t-solid gl-border-t-gray-100">
<gl-alert v-if="error" class="gl-mt-5" variant="danger" :dismissible="false">
{{ error }}
</gl-alert>
<gl-loading-icon v-if="loading" size="lg" class="gl-mt-5" />
<gl-form v-if="renderForm" @submit.prevent="onSubmit">
<gl-form-group
:label="$options.i18n.titleInputLabel"
:invalid-feedback="$options.i18n.titleInputInvalid"
:state="isValidName"
data-testid="name-input-group"
>
<template #description>
<gl-sprintf :message="$options.i18n.titleInputDescription">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
<template #link="{ content }">
<gl-link :href="scopedLabelsHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
<gl-form-input :value="name" data-testid="name-input" @input="name = $event" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.descriptionInputLabel"
:invalid-feedback="$options.i18n.descriptionInputInvalid"
:state="isValidDescription"
data-testid="description-input-group"
>
<gl-form-input
:value="description"
data-testid="description-input"
@input="description = $event"
/>
</gl-form-group>
<color-picker
:value="color"
:label="$options.i18n.colorInputLabel"
:set-color="color || ''"
:state="isValidColor"
@input="color = $event"
/>
<div
class="gl-display-flex gl-justify-content-space-between gl-pt-5 gl-border-t-1 gl-border-t-solid gl-border-t-gray-100"
>
<gl-button
type="submit"
variant="success"
class="js-no-auto-disable"
data-testid="submit-btn"
:disabled="disableSubmitBtn"
>{{ $options.i18n.submitBtnText }}</gl-button
>
<gl-button :href="groupEditPath" data-testid="cancel-btn">{{
$options.i18n.cancelBtnText
}}</gl-button>
</div>
</gl-form>
</div>
</template>
import { GlAlert, GlLoadingIcon, GlForm, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlFormGroup } from 'jest/registry/shared/stubs';
import SharedForm from 'ee/groups/settings/compliance_frameworks/components/shared_form.vue';
import ColorPicker from '~/vue_shared/components/color_picker/color_picker.vue';
import { frameworkFoundResponse } from '../mock_data';
describe('Form', () => {
let wrapper;
const defaultPropsData = { groupEditPath: 'group-1' };
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findForm = () => wrapper.findComponent(GlForm);
const findNameGroup = () => wrapper.find('[data-testid="name-input-group"]');
const findNameInput = () => wrapper.find('[data-testid="name-input"]');
const findDescriptionGroup = () => wrapper.find('[data-testid="description-input-group"]');
const findDescriptionInput = () => wrapper.find('[data-testid="description-input"]');
const findColorPicker = () => wrapper.findComponent(ColorPicker);
const findSubmitBtn = () => wrapper.find('[data-testid="submit-btn"]');
const findCancelBtn = () => wrapper.find('[data-testid="cancel-btn"]');
function createComponent(props = {}) {
return shallowMount(SharedForm, {
propsData: {
...defaultPropsData,
...props,
},
stubs: {
GlLoadingIcon,
GlFormGroup,
GlSprintf,
},
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('Loading', () => {
it.each`
loading
${true}
${false}
`('renders the app correctly', ({ loading }) => {
wrapper = createComponent({ loading });
expect(findLoadingIcon().exists()).toBe(loading);
expect(findAlert().exists()).toBe(false);
});
});
describe('Rendering the form', () => {
it.each`
renderForm
${true}
${false}
`('renders the app correctly when the renderForm prop is passed', ({ renderForm }) => {
wrapper = createComponent({ renderForm });
expect(findLoadingIcon().exists()).toBe(false);
expect(findAlert().exists()).toBe(false);
expect(findForm().exists()).toBe(renderForm);
});
});
describe('Error alert', () => {
it('shows the alert when an error are passed in', () => {
wrapper = createComponent({ error: 'Bad things happened' });
expect(findAlert().text()).toBe('Bad things happened');
});
});
describe('Fields', () => {
it('shows the correct input and button fields', () => {
wrapper = createComponent();
expect(findLoadingIcon().exists()).toBe(false);
expect(findNameInput()).toExist();
expect(findDescriptionInput()).toExist();
expect(findColorPicker()).toExist();
expect(findSubmitBtn()).toExist();
expect(findCancelBtn()).toExist();
});
it('shows the name input description', () => {
wrapper = createComponent();
expect(findNameGroup().text()).toContain('Use :: to create a scoped set (eg. SOX::AWS)');
});
});
describe('Validation', () => {
it('throws an error if the provided compliance framework is invalid', () => {
expect(SharedForm.props.complianceFramework.validator({ foo: 'bar' })).toBe(false);
});
it.each`
name | validity
${null} | ${null}
${''} | ${false}
${'foobar'} | ${true}
`('sends the correct state to the name input group', async ({ name, validity }) => {
wrapper = createComponent();
await findNameInput().vm.$emit('input', name);
expect(findNameGroup().props('state')).toBe(validity);
});
it.each`
description | validity
${null} | ${null}
${''} | ${false}
${'foobar'} | ${true}
`(
'sends the correct state to the description input group',
async ({ description, validity }) => {
wrapper = createComponent();
await findDescriptionInput().vm.$emit('input', description);
expect(findDescriptionGroup().props('state')).toBe(validity);
},
);
it.each`
color | validity
${null} | ${null}
${''} | ${null}
${'foobar'} | ${false}
${'#00'} | ${false}
${'#000'} | ${true}
${'#000000'} | ${true}
`('sends the correct state to the color picker', async ({ color, validity }) => {
wrapper = createComponent();
const colorPicker = findColorPicker();
await colorPicker.vm.$emit('input', color);
expect(colorPicker.props('state')).toBe(validity);
});
it.each`
name | description | color | disabled
${null} | ${null} | ${null} | ${'true'}
${''} | ${null} | ${null} | ${'true'}
${null} | ${''} | ${null} | ${'true'}
${null} | ${null} | ${''} | ${'true'}
${'Foo'} | ${null} | ${''} | ${'true'}
${'Foo'} | ${'Bar'} | ${'#000'} | ${undefined}
`(
'should set the submit buttons disabled attribute to $disabled',
async ({ name, description, color, disabled }) => {
wrapper = createComponent();
await findNameInput().vm.$emit('input', name);
await findDescriptionInput().vm.$emit('input', description);
await findColorPicker().vm.$emit('input', color);
expect(findSubmitBtn().attributes('disabled')).toBe(disabled);
},
);
});
describe('Updating data', () => {
it('updates the initial form data when the compliance framework prop is updated', async () => {
wrapper = createComponent();
expect(findNameInput().attributes('value')).toBe(undefined);
expect(findDescriptionInput().attributes('value')).toBe(undefined);
expect(findColorPicker().attributes('value')).toBe(undefined);
await wrapper.setProps({ complianceFramework: frameworkFoundResponse });
expect(findNameInput().attributes('value')).toBe(frameworkFoundResponse.name);
expect(findDescriptionInput().attributes('value')).toBe(frameworkFoundResponse.description);
expect(findColorPicker().attributes('value')).toBe(frameworkFoundResponse.color);
});
});
describe('On form submission', () => {
it('emits the entered form data', async () => {
wrapper = createComponent();
await findNameInput().vm.$emit('input', 'Foo');
await findDescriptionInput().vm.$emit('input', 'Bar');
await findColorPicker().vm.$emit('input', '#000');
await findForm().vm.$emit('submit', { preventDefault: () => {} });
expect(wrapper.emitted('submit')).toHaveLength(1);
expect(wrapper.emitted('submit')[0]).toEqual([
{ name: 'Foo', description: 'Bar', color: '#000' },
]);
});
it('does not emit the initial form data if editing has taken place', async () => {
wrapper = createComponent({ complianceFramework: frameworkFoundResponse });
await findNameInput().vm.$emit('input', 'Foo');
await findDescriptionInput().vm.$emit('input', 'Bar');
await findColorPicker().vm.$emit('input', '#000');
await findForm().vm.$emit('submit', { preventDefault: () => {} });
expect(wrapper.emitted('submit')).toHaveLength(1);
expect(wrapper.emitted('submit')[0]).toEqual([
{ name: 'Foo', description: 'Bar', color: '#000' },
]);
});
});
});
......@@ -40,3 +40,11 @@ export const emptyFetchResponse = {
},
},
};
export const frameworkFoundResponse = {
id: 'gid://gitlab/ComplianceManagement::Framework/1',
name: 'GDPR',
description: 'General Data Protection Regulation',
color: '#1aaa55',
parsedId: 1,
};
......@@ -1253,6 +1253,9 @@ msgstr ""
msgid "A deleted user"
msgstr ""
msgid "A description is required"
msgstr ""
msgid "A file has been changed."
msgstr ""
......@@ -1346,6 +1349,9 @@ msgstr ""
msgid "A string appended to the project path to form the Service Desk email address."
msgstr ""
msgid "A title is required"
msgstr ""
msgid "A user can only participate in a rotation once"
msgstr ""
......@@ -7326,6 +7332,9 @@ msgstr ""
msgid "ComplianceFrameworks|There are no compliance frameworks set up yet"
msgstr ""
msgid "ComplianceFrameworks|Use %{codeStart}::%{codeEnd} to create a %{linkStart}scoped set%{linkEnd} (eg. %{codeStart}SOX::AWS%{codeEnd})"
msgstr ""
msgid "ComplianceFramework|GDPR"
msgstr ""
......
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