Commit bc8ea9a8 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '297370-persist-value-stream-from-defaults' into 'master'

Persist value stream stages

See merge request gitlab-org/gitlab!51884
parents 08ce2b85 98fb79db
......@@ -71,13 +71,7 @@ export const defaultFields = {
endEventLabelId: null,
};
const defaultStageCommonFields = { custom: false, hidden: false };
export const defaultCustomStageFields = {
...defaultFields,
...defaultStageCommonFields,
custom: true,
};
export const defaultCustomStageFields = { ...defaultFields, custom: true };
/**
* These stage configs are copied from the https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/cycle_analytics
......@@ -120,11 +114,12 @@ const BASE_DEFAULT_STAGE_CONFIG = [
export const DEFAULT_STAGE_CONFIG = BASE_DEFAULT_STAGE_CONFIG.map(({ id, ...rest }) => ({
...rest,
...defaultStageCommonFields,
custom: false,
name: capitalizeFirstCharacter(id),
}));
export const PRESET_OPTIONS_DEFAULT = 'default';
export const PRESET_OPTIONS_BLANK = 'blank';
export const PRESET_OPTIONS = [
{
text: I18N.TEMPLATE_DEFAULT,
......@@ -132,6 +127,6 @@ export const PRESET_OPTIONS = [
},
{
text: I18N.TEMPLATE_BLANK,
value: 'blank',
value: PRESET_OPTIONS_BLANK,
},
];
......@@ -32,7 +32,7 @@ export default {
errors: {
type: Object,
required: false,
default: () => {},
default: () => ({}),
},
stageEvents: {
type: Array,
......
......@@ -40,7 +40,7 @@ export default {
errors: {
type: Object,
required: false,
default: () => {},
default: () => ({}),
},
stageEvents: {
type: Array,
......@@ -49,10 +49,10 @@ export default {
},
methods: {
isValid(field) {
return !this.errors[field]?.length;
return !this.errors[field] || !this.errors[field]?.length;
},
renderError(field) {
return this.errors[field]?.join('\n');
return this.errors[field] ? this.errors[field]?.join('\n') : null;
},
eventName(eventIds = []) {
return eventIdsToName(this.stageEvents, eventIds);
......@@ -68,6 +68,7 @@ export default {
class="gl-flex-grow-1 gl-mb-0"
:state="isValid('name')"
:invalid-feedback="renderError('name')"
:data-testid="`default-stage-name-${index}`"
>
<!-- eslint-disable vue/no-mutating-props -->
<gl-form-input
......
......@@ -88,20 +88,16 @@ export const validateStage = (fields) => {
if (fields?.name) {
if (fields.name.length > NAME_MAX_LENGTH) {
newErrors.name = [ERRORS.MAX_LENGTH];
} else {
newErrors.name =
fields?.custom && DEFAULT_STAGE_NAMES.includes(fields.name.toLowerCase())
? [ERRORS.STAGE_NAME_EXISTS]
: [];
}
if (fields?.custom && DEFAULT_STAGE_NAMES.includes(fields.name.toLowerCase())) {
newErrors.name = [ERRORS.STAGE_NAME_EXISTS];
}
} else {
newErrors.name = [ERRORS.STAGE_NAME_MIN_LENGTH];
}
if (fields?.startEventIdentifier) {
if (fields?.endEventIdentifier) {
newErrors.endEventIdentifier = [];
} else {
if (!fields?.endEventIdentifier) {
newErrors.endEventIdentifier = [ERRORS.END_EVENT_REQUIRED];
}
} else {
......@@ -115,15 +111,15 @@ export const validateStage = (fields) => {
* returned as an array in a object with key`name`
*
* @param {Object} fields key value pair of form field values
* @returns {Object} key value pair of form fields with an array of errors
* @returns {Array} an array of errors
*/
export const validateValueStreamName = ({ name = '' }) => {
const errors = { name: [] };
const errors = [];
if (name.length > NAME_MAX_LENGTH) {
errors.name.push(ERRORS.MAX_LENGTH);
errors.push(ERRORS.MAX_LENGTH);
}
if (!name.length) {
errors.name.push(ERRORS.VALUE_STREAM_NAME_MIN_LENGTH);
errors.push(ERRORS.VALUE_STREAM_NAME_MIN_LENGTH);
}
return errors;
};
<script>
import Vue from 'vue';
import { mapGetters, mapState } from 'vuex';
import { isEqual } from 'lodash';
import {
......@@ -163,7 +164,7 @@ export default {
this.fields.startEventIdentifier && this.eventMismatchError
? [ERRORS.INVALID_EVENT_PAIRS]
: newErrors.endEventIdentifier;
this.errors = { ...this.errors, ...newErrors };
Vue.set(this, 'errors', newErrors);
},
},
I18N,
......
......@@ -3,6 +3,7 @@ import Vue from 'vue';
import { GlButton, GlForm, GlFormInput, GlFormGroup, GlFormRadioGroup, GlModal } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { sprintf } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import { swapArrayItems } from '~/lib/utils/array_utility';
import {
DEFAULT_STAGE_CONFIG,
......@@ -16,12 +17,24 @@ import { validateValueStreamName, validateStage } from './create_value_stream_fo
import DefaultStageFields from './create_value_stream_form/default_stage_fields.vue';
import CustomStageFields from './create_value_stream_form/custom_stage_fields.vue';
const findStageIndexByName = (stages, target = '') =>
stages.findIndex(({ name }) => name === target);
const initializeStageErrors = (selectedPreset = PRESET_OPTIONS_DEFAULT) =>
selectedPreset === PRESET_OPTIONS_DEFAULT ? DEFAULT_STAGE_CONFIG.map(() => ({})) : [{}];
const initializeStages = (selectedPreset = PRESET_OPTIONS_DEFAULT) =>
selectedPreset === PRESET_OPTIONS_DEFAULT
? DEFAULT_STAGE_CONFIG
: [{ ...defaultCustomStageFields }];
const formatStageDataForSubmission = (stages) => {
return stages.map(({ custom = false, name, ...rest }) => {
return custom
? convertObjectPropsToSnakeCase({ ...rest, custom, name })
: {
name,
};
});
};
export default {
name: 'ValueStreamForm',
components: {
......@@ -40,6 +53,16 @@ export default {
required: false,
default: () => ({}),
},
initialPreset: {
type: String,
required: false,
default: PRESET_OPTIONS_DEFAULT,
},
initialFormErrors: {
type: Object,
required: false,
default: () => ({}),
},
hasExtendedFormFields: {
type: Boolean,
required: false,
......@@ -47,53 +70,48 @@ export default {
},
},
data() {
const { hasExtendedFormFields, initialData } = this;
const { hasExtendedFormFields, initialData, initialFormErrors, initialPreset } = this;
const { name: nameError = [], stages: stageErrors = [{}] } = initialFormErrors;
const additionalFields = hasExtendedFormFields
? {
stages: DEFAULT_STAGE_CONFIG,
stageErrors: initializeStageErrors(PRESET_OPTIONS_DEFAULT),
stages: initializeStages(initialPreset),
stageErrors: stageErrors || initializeStageErrors(initialPreset),
...initialData,
}
: { stages: [] };
: { stages: [], nameError };
return {
selectedPreset: PRESET_OPTIONS[0].value,
hiddenStages: [],
selectedPreset: initialPreset,
presetOptions: PRESET_OPTIONS,
name: '',
nameError: { name: [] },
stageErrors: [{}],
nameError,
stageErrors,
...additionalFields,
};
},
computed: {
...mapState({
initialFormErrors: 'createValueStreamErrors',
isCreating: 'isCreatingValueStream',
}),
...mapState('customStages', ['formEvents']),
isValueStreamNameValid() {
return !this.nameError.name?.length;
return !this.nameError?.length;
},
invalidFeedback() {
return this.nameError.name?.join('\n');
invalidNameFeedback() {
return this.nameError?.length ? this.nameError.join('\n\n') : null;
},
hasInitialFormErrors() {
const { initialFormErrors } = this;
return Boolean(Object.keys(initialFormErrors).length);
},
isValid() {
return this.isValueStreamNameValid && !this.hasInitialFormErrors;
},
isLoading() {
return this.isCreating;
},
primaryProps() {
return {
text: this.$options.I18N.FORM_TITLE,
attributes: [
{ variant: 'success' },
{ disabled: !this.isValid },
{ loading: this.isLoading },
],
attributes: [{ variant: 'success' }, { loading: this.isLoading }],
};
},
secondaryProps() {
......@@ -106,41 +124,35 @@ export default {
],
};
},
hiddenStages() {
return this.stages.filter((stage) => stage.hidden);
},
activeStages() {
return this.stages.filter((stage) => !stage.hidden);
hasFormErrors() {
return Boolean(
this.nameError.length || this.stageErrors.some((obj) => Object.keys(obj).length),
);
},
},
watch: {
initialFormErrors(newErrors = {}) {
this.stageErrors = newErrors;
initialFormErrors({ name: nameError, stages: stageErrors }) {
Vue.set(this, 'nameError', nameError);
Vue.set(this, 'stageErrors', stageErrors);
},
},
mounted() {
const { initialFormErrors } = this;
if (this.hasInitialFormErrors) {
this.stageErrors = initialFormErrors;
}
},
methods: {
...mapActions(['createValueStream']),
onSubmit() {
const { name, stages } = this;
this.validate();
if (this.hasFormErrors) return false;
return this.createValueStream({
name,
stages: stages.map(({ name: stageName, ...rest }) => ({
name: stageName,
...rest,
title: stageName,
})),
name: this.name,
stages: formatStageDataForSubmission(this.stages),
}).then(() => {
if (!this.hasInitialFormErrors) {
this.$toast.show(sprintf(this.$options.I18N.FORM_CREATED, { name }), {
this.$toast.show(sprintf(this.$options.I18N.FORM_CREATED, { name: this.name }), {
position: 'top-center',
});
this.name = '';
this.nameError = [];
this.stages = initializeStages(this.selectedPreset);
this.stageErrors = initializeStageErrors(this.selectedPreset);
}
});
},
......@@ -156,7 +168,7 @@ export default {
return sprintf(this.$options.I18N.HIDDEN_DEFAULT_STAGE, { name });
},
validateStages() {
return this.activeStages.map(validateStage);
return this.stages.map(validateStage);
},
validate() {
const { name } = this;
......@@ -175,14 +187,15 @@ export default {
Vue.set(this, 'stages', newStages);
},
validateStageFields(index) {
Vue.set(this.stageErrors, index, validateStage(this.activeStages[index]));
Vue.set(this.stageErrors, index, validateStage(this.stages[index]));
},
fieldErrors(index) {
return this.stageErrors[index];
return this.stageErrors && this.stageErrors[index] ? this.stageErrors[index] : {};
},
onHide(index) {
const stage = this.stages[index];
Vue.set(this.stages, index, { ...stage, hidden: true });
const target = this.stages[index];
Vue.set(this, 'stages', [...this.stages.filter((_, i) => i !== index)]);
Vue.set(this, 'hiddenStages', [...this.hiddenStages, target]);
},
onRemove(index) {
const newErrors = this.stageErrors.filter((_, idx) => idx !== index);
......@@ -191,9 +204,11 @@ export default {
Vue.set(this, 'stageErrors', [...newErrors]);
},
onRestore(hiddenStageIndex) {
const stage = this.hiddenStages[hiddenStageIndex];
const stageIndex = findStageIndexByName(this.stages, stage.name);
Vue.set(this.stages, stageIndex, { ...stage, hidden: false });
const target = this.hiddenStages[hiddenStageIndex];
Vue.set(this, 'hiddenStages', [
...this.hiddenStages.filter((_, i) => i !== hiddenStageIndex),
]);
Vue.set(this, 'stages', [...this.stages, target]);
},
onAddStage() {
// validate previous stages only and add a new stage
......@@ -242,9 +257,10 @@ export default {
>
<gl-form>
<gl-form-group
data-testid="create-value-stream-name"
label-for="create-value-stream-name"
:label="$options.I18N.FORM_FIELD_NAME_LABEL"
:invalid-feedback="invalidFeedback"
:invalid-feedback="invalidNameFeedback"
:state="isValueStreamNameValid"
>
<div class="gl-display-flex gl-justify-content-space-between">
......@@ -275,7 +291,7 @@ export default {
@input="onSelectPreset"
/>
<div v-if="hasExtendedFormFields" data-testid="extended-form-fields">
<div v-for="(stage, activeStageIndex) in activeStages" :key="stageKey(activeStageIndex)">
<div v-for="(stage, activeStageIndex) in stages" :key="stageKey(activeStageIndex)">
<hr class="gl-my-3" />
<span
class="gl-display-flex gl-m-0 gl-vertical-align-middle gl-mr-2 gl-font-weight-bold gl-display-flex gl-pb-3"
......@@ -286,7 +302,7 @@ export default {
:stage="stage"
:stage-events="formEvents"
:index="activeStageIndex"
:total-stages="activeStages.length"
:total-stages="stages.length"
:errors="fieldErrors(activeStageIndex)"
@move="handleMove"
@remove="onRemove"
......@@ -297,7 +313,7 @@ export default {
:stage="stage"
:stage-events="formEvents"
:index="activeStageIndex"
:total-stages="activeStages.length"
:total-stages="stages.length"
:errors="fieldErrors(activeStageIndex)"
@move="handleMove"
@hide="onHide"
......
......@@ -47,6 +47,7 @@ export default {
deleteValueStreamError: 'deleteValueStreamError',
data: 'valueStreams',
selectedValueStream: 'selectedValueStream',
initialFormErrors: 'createValueStreamErrors',
}),
hasValueStreams() {
return Boolean(this.data.length);
......@@ -123,7 +124,10 @@ export default {
<gl-button v-else v-gl-modal-directive="'value-stream-form-modal'">{{
$options.I18N.CREATE_VALUE_STREAM
}}</gl-button>
<value-stream-form :has-extended-form-fields="hasExtendedFormFields" />
<value-stream-form
:initial-form-errors="initialFormErrors"
:has-extended-form-fields="hasExtendedFormFields"
/>
<gl-modal
data-testid="delete-value-stream-modal"
modal-id="delete-value-stream-modal"
......
......@@ -41,9 +41,8 @@ export const setDateRange = ({ commit, dispatch }, { skipFetch = false, startDat
};
export const requestStageData = ({ commit }) => commit(types.REQUEST_STAGE_DATA);
export const receiveStageDataSuccess = ({ commit }, data) => {
export const receiveStageDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_STAGE_DATA_SUCCESS, data);
};
export const receiveStageDataError = ({ commit }, error) => {
const { message = '' } = error;
......@@ -351,7 +350,7 @@ export const createValueStream = ({ commit, dispatch, getters }, data) => {
.then(({ data: newValueStream }) => dispatch('receiveCreateValueStreamSuccess', newValueStream))
.catch(({ response } = {}) => {
const { data: { message, payload: { errors } } = null } = response;
commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { message, errors });
commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { message, errors, data });
});
};
......
......@@ -66,6 +66,8 @@ export default {
},
[types.RECEIVE_CREATE_STAGE_ERROR](state) {
state.isSavingCustomStage = false;
state.isCreatingCustomStage = false;
state.isEditingCustomStage = false;
},
[types.RECEIVE_CREATE_STAGE_SUCCESS](state) {
state.formErrors = null;
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { transformRawStages } from '../utils';
import * as types from './mutation_types';
import { transformRawStages, prepareStageErrors } from '../utils';
export default {
[types.SET_FEATURE_FLAGS](state, featureFlags) {
......@@ -67,7 +67,8 @@ export default {
state.stages = [];
},
[types.RECEIVE_GROUP_STAGES_SUCCESS](state, stages) {
state.stages = transformRawStages(stages);
const transformedStages = transformRawStages(stages);
state.stages = transformedStages.sort((a, b) => a?.id > b?.id);
},
[types.REQUEST_UPDATE_STAGE](state) {
state.isLoading = true;
......@@ -120,9 +121,10 @@ export default {
state.isCreatingValueStream = true;
state.createValueStreamErrors = {};
},
[types.RECEIVE_CREATE_VALUE_STREAM_ERROR](state, { errors } = {}) {
[types.RECEIVE_CREATE_VALUE_STREAM_ERROR](state, { data: { stages = [] }, errors = {} }) {
const { stages: stageErrors = {}, ...rest } = errors;
state.createValueStreamErrors = { ...rest, stages: prepareStageErrors(stages, stageErrors) };
state.isCreatingValueStream = false;
state.createValueStreamErrors = errors;
},
[types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS](state, valueStream) {
state.isCreatingValueStream = false;
......
......@@ -105,6 +105,19 @@ export const transformRawTasksByTypeData = (data = []) => {
return data.map((d) => convertObjectPropsToCamelCase(d, { deep: true }));
};
/**
* Prepares the stage errors for use in the create value stream form
*
* The JSON error response returns a key value pair, the key corresponds to the
* index of the stage with errors and the value is the returned error(s)
*
* @param {Array} stages - Array of value stream stages
* @param {Object} errors - Key value pair of stage errors
* @returns {Array} Returns and array of stage error objects
*/
export const prepareStageErrors = (stages, errors) =>
stages.length ? stages.map((_, index) => convertObjectPropsToCamelCase(errors[index]) || {}) : [];
/**
* Takes the duration data for selected stages, transforms the date values and returns
* the data in a flattened array
......
......@@ -111,7 +111,7 @@ describe('validateStage', () => {
describe('validateValueStreamName,', () => {
it('with valid data returns an empty array', () => {
expect(validateValueStreamName({ name: 'Cool stream name' })).toEqual({ name: [] });
expect(validateValueStreamName({ name: 'Cool stream name' })).toEqual([]);
});
it.each`
......@@ -120,6 +120,6 @@ describe('validateValueStreamName,', () => {
${''} | ${ERRORS.VALUE_STREAM_NAME_MIN_LENGTH} | ${'too short'}
`('returns "$error" if name is $msg', ({ name, error }) => {
const result = validateValueStreamName({ name });
expectFieldError({ result, error, field: 'name' });
expect(result).toEqual([error]);
});
});
......@@ -133,7 +133,7 @@ describe('CustomStageForm', () => {
it('clears the error when the field changes', async () => {
await setNameField('not an issue');
expect(findFieldErrors('name')).not.toContain('Stage name already exists');
expect(findFieldErrors('name')).toBeUndefined();
});
});
});
......
......@@ -2,6 +2,9 @@ import { GlModal } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import ValueStreamForm from 'ee/analytics/cycle_analytics/components/value_stream_form.vue';
import DefaultStageFields from 'ee/analytics/cycle_analytics/components/create_value_stream_form/default_stage_fields.vue';
import CustomStageFields from 'ee/analytics/cycle_analytics/components/create_value_stream_form/custom_stage_fields.vue';
import { PRESET_OPTIONS_BLANK } from 'ee/analytics/cycle_analytics/components/create_value_stream_form/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { customStageEvents as formEvents } from '../mock_data';
......@@ -15,13 +18,20 @@ describe('ValueStreamForm', () => {
const mockEvent = { preventDefault: jest.fn() };
const mockToastShow = jest.fn();
const streamName = 'Cool stream';
const createValueStreamErrors = { name: ['Name field required'] };
const initialFormErrors = { name: ['Name field required'] };
const initialFormStageErrors = {
stages: [
{
name: ['Name field is required'],
endEventIdentifier: ['Please select a start event first'],
},
],
};
const fakeStore = ({ initialState = {} }) =>
new Vuex.Store({
state: {
isCreatingValueStream: false,
createValueStreamErrors: {},
...initialState,
},
actions: {
......@@ -37,7 +47,7 @@ describe('ValueStreamForm', () => {
},
});
const createComponent = ({ props = {}, data = {}, initialState = {} } = {}) =>
const createComponent = ({ props = {}, data = {}, initialState = {}, stubs = {} } = {}) =>
extendedWrapper(
shallowMount(ValueStreamForm, {
localVue,
......@@ -55,6 +65,9 @@ describe('ValueStreamForm', () => {
show: mockToastShow,
},
},
stubs: {
...stubs,
},
}),
);
......@@ -66,6 +79,8 @@ describe('ValueStreamForm', () => {
const findBtn = (btn) => findModal().props(btn);
const findSubmitDisabledAttribute = (attribute) =>
findBtn('actionPrimary').attributes[1][attribute];
const expectFieldError = (testId, error = '') =>
expect(wrapper.findByTestId(testId).attributes('invalid-feedback')).toBe(error);
afterEach(() => {
wrapper.destroy();
......@@ -78,7 +93,7 @@ describe('ValueStreamForm', () => {
});
it('submit button is enabled', () => {
expect(findSubmitDisabledAttribute('disabled')).toBe(false);
expect(findSubmitDisabledAttribute('disabled')).toBeUndefined();
});
it('does not include extended fields', () => {
......@@ -123,21 +138,67 @@ describe('ValueStreamForm', () => {
expect(wrapper.vm.stages.length).toBe(7);
});
it('validates existing fields when clicked', () => {
expect(wrapper.vm.nameError).toEqual([]);
clickAddStage();
expect(wrapper.vm.nameError).toEqual(['Name is required']);
});
});
describe('form errors', () => {
const commonExtendedData = {
props: {
hasExtendedFormFields: true,
initialFormErrors: initialFormStageErrors,
},
};
it('renders errors for a default stage field', () => {
wrapper = createComponent({
...commonExtendedData,
stubs: {
DefaultStageFields,
},
});
expectFieldError('default-stage-name-0', initialFormStageErrors.stages[0].name[0]);
});
it('renders errors for a custom stage field', async () => {
wrapper = createComponent({
props: {
...commonExtendedData.props,
initialPreset: PRESET_OPTIONS_BLANK,
},
stubs: {
CustomStageFields,
},
});
expectFieldError('custom-stage-name-0', initialFormStageErrors.stages[0].name[0]);
expectFieldError(
'custom-stage-end-event-0',
initialFormStageErrors.stages[0].endEventIdentifier[0],
);
});
});
});
describe('form errors', () => {
beforeEach(() => {
wrapper = createComponent({
data: { name: streamName },
initialState: {
createValueStreamErrors,
data: { name: '' },
props: {
initialFormErrors,
},
});
});
it('submit button is disabled', () => {
expect(findSubmitDisabledAttribute('disabled')).toBe(true);
it('renders errors for the name field', () => {
expectFieldError('create-value-stream-name', initialFormErrors.name[0]);
});
});
......@@ -146,10 +207,6 @@ describe('ValueStreamForm', () => {
wrapper = createComponent({ data: { name: streamName } });
});
it('submit button is enabled', () => {
expect(findSubmitDisabledAttribute('disabled')).toBe(false);
});
describe('form submitted successfully', () => {
beforeEach(() => {
clickSubmit();
......@@ -177,8 +234,8 @@ describe('ValueStreamForm', () => {
beforeEach(() => {
wrapper = createComponent({
data: { name: streamName },
initialState: {
createValueStreamErrors,
props: {
initialFormErrors,
},
});
......
......@@ -948,7 +948,7 @@ describe('Value Stream Analytics actions', () => {
{ type: types.REQUEST_CREATE_VALUE_STREAM },
{
type: types.RECEIVE_CREATE_VALUE_STREAM_ERROR,
payload: { message, errors },
payload: { message, data: payload, errors },
},
],
[],
......
......@@ -15,6 +15,7 @@ import {
} from '../mock_data';
let state = null;
const { stages } = customizableStagesAndEvents;
describe('Value Stream Analytics mutations', () => {
beforeEach(() => {
......@@ -61,16 +62,16 @@ describe('Value Stream Analytics mutations', () => {
});
it.each`
mutation | payload | expectedState
${types.SET_FEATURE_FLAGS} | ${{ hasDurationChart: true }} | ${{ featureFlags: { hasDurationChart: true } }}
${types.SET_SELECTED_PROJECTS} | ${selectedProjects} | ${{ selectedProjects }}
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }}
${types.RECEIVE_CREATE_VALUE_STREAM_ERROR} | ${{ errors: { name: ['is required'] } }} | ${{ createValueStreamErrors: { name: ['is required'] }, isCreatingValueStream: false }}
${types.RECEIVE_DELETE_VALUE_STREAM_ERROR} | ${'Some error occurred'} | ${{ deleteValueStreamError: 'Some error occurred' }}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${valueStreams} | ${{ valueStreams, isLoadingValueStreams: false }}
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: {} }}
${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} | ${valueStreams[1]} | ${{ selectedValueStream: valueStreams[1] }}
mutation | payload | expectedState
${types.SET_FEATURE_FLAGS} | ${{ hasDurationChart: true }} | ${{ featureFlags: { hasDurationChart: true } }}
${types.SET_SELECTED_PROJECTS} | ${selectedProjects} | ${{ selectedProjects }}
${types.SET_DATE_RANGE} | ${{ startDate, endDate }} | ${{ startDate, endDate }}
${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }}
${types.RECEIVE_CREATE_VALUE_STREAM_ERROR} | ${{ data: { stages }, errors: { name: ['is required'] } }} | ${{ createValueStreamErrors: { name: ['is required'] }, isCreatingValueStream: false }}
${types.RECEIVE_DELETE_VALUE_STREAM_ERROR} | ${'Some error occurred'} | ${{ deleteValueStreamError: 'Some error occurred' }}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${valueStreams} | ${{ valueStreams, isLoadingValueStreams: false }}
${types.SET_SELECTED_VALUE_STREAM} | ${valueStreams[1].id} | ${{ selectedValueStream: {} }}
${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} | ${valueStreams[1]} | ${{ selectedValueStream: valueStreams[1] }}
`(
'$mutation with payload $payload will update state with $expectedState',
({ mutation, payload, expectedState }) => {
......@@ -126,7 +127,7 @@ describe('Value Stream Analytics mutations', () => {
describe(`${types.RECEIVE_GROUP_STAGES_SUCCESS}`, () => {
describe('with data', () => {
beforeEach(() => {
mutations[types.RECEIVE_GROUP_STAGES_SUCCESS](state, customizableStagesAndEvents.stages);
mutations[types.RECEIVE_GROUP_STAGES_SUCCESS](state, stages);
});
it('will convert the stats object to stages', () => {
......@@ -180,14 +181,11 @@ describe('Value Stream Analytics mutations', () => {
${'selectedProjects'} | ${initialData.selectedProjects}
${'startDate'} | ${initialData.createdAfter}
${'endDate'} | ${initialData.createdBefore}
`(
'$mutation with payload $payload will update state with $expectedState',
({ stateKey, expectedState }) => {
state = {};
mutations[types.INITIALIZE_CYCLE_ANALYTICS](state, initialData);
`('$stateKey will be set to $expectedState', ({ stateKey, expectedState }) => {
state = {};
mutations[types.INITIALIZE_CYCLE_ANALYTICS](state, initialData);
expect(state[stateKey]).toEqual(expectedState);
},
);
expect(state[stateKey]).toEqual(expectedState);
});
});
});
......@@ -16,6 +16,7 @@ import {
toggleSelectedLabel,
transformStagesForPathNavigation,
prepareTimeMetricsData,
prepareStageErrors,
} from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
......@@ -176,6 +177,29 @@ describe('Value Stream Analytics utils', () => {
});
});
describe('prepareStageErrors', () => {
const stages = [{ name: 'stage 1' }, { name: 'stage 2' }, { name: 'stage 3' }];
const nameError = { name: "Can't be blank" };
const stageErrors = { 1: nameError };
it('returns an object for each stage', () => {
const res = prepareStageErrors(stages, stageErrors);
expect(res[0]).toEqual({});
expect(res[1]).toEqual(nameError);
expect(res[2]).toEqual({});
});
it('returns the same number of error objects as stages', () => {
const res = prepareStageErrors(stages, stageErrors);
expect(res.length).toEqual(stages.length);
});
it('returns an empty object for each stage if there are no errors', () => {
const res = prepareStageErrors(stages, {});
expect(res).toEqual([{}, {}, {}]);
});
});
describe('isPersistedStage', () => {
it.each`
custom | id | expected
......
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