diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions/value_streams.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions/value_streams.js index 92d58e449f564d0a6a8411013c01a37edfee9f75..34538026f62c8b10be19573594f7d1daa3ea046d 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions/value_streams.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/actions/value_streams.js @@ -1,31 +1,6 @@ -import { - createValueStream as apiCreateValueStream, - updateValueStream as apiUpdateValueStream, - deleteValueStream as apiDeleteValueStream, - getValueStreams, -} from 'ee/api/analytics_api'; +import { deleteValueStream as apiDeleteValueStream, getValueStreams } from 'ee/api/analytics_api'; import * as types from '../mutation_types'; -export const createValueStream = ({ commit, getters }, data) => { - const { namespaceRestApiRequestPath } = getters; - commit(types.REQUEST_CREATE_VALUE_STREAM); - - return apiCreateValueStream(namespaceRestApiRequestPath, data).catch(({ response } = {}) => { - const { data: { message, payload: { errors } } = null } = response; - commit(types.RECEIVE_CREATE_VALUE_STREAM_ERROR, { message, errors, data }); - }); -}; - -export const updateValueStream = ({ commit, getters }, { id: valueStreamId, ...data }) => { - const { namespaceRestApiRequestPath: namespacePath } = getters; - commit(types.REQUEST_UPDATE_VALUE_STREAM); - - return apiUpdateValueStream({ namespacePath, valueStreamId, data }).catch(({ response } = {}) => { - const { data: { message, payload: { errors } } = null } = response; - commit(types.RECEIVE_UPDATE_VALUE_STREAM_ERROR, { message, errors, data }); - }); -}; - export const deleteValueStream = ({ commit, dispatch, getters }, valueStreamId) => { const { namespaceRestApiRequestPath } = getters; commit(types.REQUEST_DELETE_VALUE_STREAM); diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js index 502ff65c703ce302095fcccf218c3a6da6a27b13..52e6a63166c99175631263901efe3e7f8e676eaf 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js @@ -35,12 +35,6 @@ export const RECEIVE_GROUP_LABELS_ERROR = 'RECEIVE_GROUP_LABELS_ERROR'; export const INITIALIZE_VSA = 'INITIALIZE_VSA'; export const INITIALIZE_VALUE_STREAM_SUCCESS = 'INITIALIZE_VALUE_STREAM_SUCCESS'; -export const REQUEST_CREATE_VALUE_STREAM = 'REQUEST_CREATE_VALUE_STREAM'; -export const RECEIVE_CREATE_VALUE_STREAM_ERROR = 'RECEIVE_CREATE_VALUE_STREAM_ERROR'; - -export const REQUEST_UPDATE_VALUE_STREAM = 'REQUEST_UPDATE_VALUE_STREAM'; -export const RECEIVE_UPDATE_VALUE_STREAM_ERROR = 'RECEIVE_UPDATE_VALUE_STREAM_ERROR'; - export const REQUEST_DELETE_VALUE_STREAM = 'REQUEST_DELETE_VALUE_STREAM'; export const RECEIVE_DELETE_VALUE_STREAM_SUCCESS = 'RECEIVE_DELETE_VALUE_STREAM_SUCCESS'; export const RECEIVE_DELETE_VALUE_STREAM_ERROR = 'RECEIVE_DELETE_VALUE_STREAM_ERROR'; diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js index e3e57e1b55b335629394b1bf1a269fc352dd317d..eeec6e52f517b5ebe5a885e89304ccc8ac88afe2 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js @@ -3,7 +3,7 @@ import { PAGINATION_SORT_DIRECTION_DESC, } from '~/analytics/cycle_analytics/constants'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { transformRawStages, prepareStageErrors, formatMedianValues } from '../utils'; +import { transformRawStages, formatMedianValues } from '../utils'; import * as types from './mutation_types'; export default { @@ -149,20 +149,6 @@ export default { [types.INITIALIZE_VALUE_STREAM_SUCCESS](state) { state.isLoading = false; }, - [types.REQUEST_CREATE_VALUE_STREAM](state) { - state.createValueStreamErrors = {}; - }, - [types.RECEIVE_CREATE_VALUE_STREAM_ERROR](state, { data: { stages = [] }, errors = {} }) { - const { stages: stageErrors = {}, ...rest } = errors; - state.createValueStreamErrors = { ...rest, stages: prepareStageErrors(stages, stageErrors) }; - }, - [types.REQUEST_UPDATE_VALUE_STREAM](state) { - state.createValueStreamErrors = {}; - }, - [types.RECEIVE_UPDATE_VALUE_STREAM_ERROR](state, { data: { stages = [] }, errors = {} }) { - const { stages: stageErrors = {}, ...rest } = errors; - state.createValueStreamErrors = { ...rest, stages: prepareStageErrors(stages, stageErrors) }; - }, [types.REQUEST_DELETE_VALUE_STREAM](state) { state.isDeletingValueStream = true; state.deleteValueStreamError = null; diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/store/state.js b/ee/app/assets/javascripts/analytics/cycle_analytics/store/state.js index 98fe4c59f0d05a0e864e30fba5e33483a3e62110..fda1284b850884360ec1e0f5277fb65257b36e08 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/store/state.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/store/state.js @@ -30,7 +30,6 @@ export default () => ({ isFetchingGroupLabels: false, isFetchingGroupStagesAndEvents: false, - createValueStreamErrors: {}, deleteValueStreamError: null, stages: [], diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/utils.js b/ee/app/assets/javascripts/analytics/cycle_analytics/utils.js index a331824be3e6a1ae1397eddf0e91ff126287bac9..2309b5ee7fa6f5e468ae6885dc07701df4e4b11a 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/utils.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/utils.js @@ -63,19 +63,6 @@ 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 diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/stories_constants.js b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/stories_constants.js index 9a1273bc43bcc187fff376d52664cb66fac0613d..af51569399e3330c882c3175f0cf9ec752076dfc 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/stories_constants.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/stories_constants.js @@ -169,12 +169,3 @@ export const selectedValueStreamStages = ({ hideStages = false, addCustomStage = ...(addCustomStage ? [customStage] : []), ...defaultStageConfig.map(({ custom, name }) => ({ custom, name, hidden: hideStages })), ]; - -export const formSubmissionErrors = { - name: ['has already been taken'], - stages: [ - { - name: ['has already been taken'], - }, - ], -}; diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.stories.js b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.stories.js index b5248877a560bc9eceba7f08eb5e136a678b201f..d6a30ba0f2edc4d2524bda34941eb2ba1b15c8c2 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.stories.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form.stories.js @@ -5,7 +5,6 @@ import { formEvents, selectedValueStream, selectedValueStreamStages, - formSubmissionErrors as createValueStreamErrors, } from './stories_constants'; import ValueStreamForm from './value_stream_form.vue'; @@ -78,19 +77,6 @@ export const EditValueStreamWithHiddenStages = { }, }; -export const WithFormSubmissionErrors = { - render: createStoryWithState({ - state: { - selectedValueStream, - stages: selectedValueStreamStages({ addCustomStage: true }), - createValueStreamErrors, - }, - }), - args: { - isEditing: true, - }, -}; - export const Loading = { render: createStoryWithState({ state: { diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue index 900ac0ceeda999dc274b892a655a8fe0051847f2..0a59b123aef0ade86933d3a4ea243e44936af96d 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue @@ -2,7 +2,7 @@ import { GlAlert, GlButton, GlForm, GlFormInput, GlFormGroup, GlFormRadioGroup } from '@gitlab/ui'; import { cloneDeep, uniqueId } from 'lodash'; // eslint-disable-next-line no-restricted-imports -import { mapState, mapActions } from 'vuex'; +import { mapState, mapGetters } from 'vuex'; import { filterStagesByHiddenStatus } from '~/analytics/cycle_analytics/utils'; import { swapArrayItems } from '~/lib/utils/array_utility'; import { sprintf } from '~/locale'; @@ -10,12 +10,14 @@ import Tracking from '~/tracking'; import CrudComponent from '~/vue_shared/components/crud_component.vue'; import { visitUrlWithAlerts, mergeUrlParams } from '~/lib/utils/url_utility'; import { getLabelEventsIdentifiers } from 'ee/analytics/cycle_analytics/utils'; +import { createValueStream, updateValueStream } from 'ee/api/analytics_api'; import { validateValueStreamName, cleanStageName, validateStage, formatStageDataForSubmission, hasDirtyStage, + prepareStageErrors, } from '../utils'; import { STAGE_SORT_DIRECTION, @@ -99,13 +101,8 @@ export default { }; }, computed: { - ...mapState([ - 'isFetchingGroupLabels', - 'formEvents', - 'defaultGroupLabels', - 'createValueStreamErrors', - 'selectedValueStream', - ]), + ...mapState(['formEvents', 'defaultGroupLabels', 'selectedValueStream']), + ...mapGetters(['namespaceRestApiRequestPath']), selectedValueStreamId() { return this.selectedValueStream?.id || -1; }, @@ -144,49 +141,53 @@ export default { return { id, message, variant: 'success' }; }, - }, - watch: { - createValueStreamErrors: 'refreshErrors', - }, - created() { - this.refreshErrors(); + submitParams() { + const { name, stages, isEditing } = this; + return { + name, + stages: formatStageDataForSubmission(stages, isEditing), + }; + }, }, methods: { - ...mapActions(['createValueStream', 'updateValueStream']), - async onSubmit() { + onSubmit() { this.showSubmitError = false; this.validate(); if (this.hasFormErrors) return; - let req = this.createValueStream; - let params = { - name: this.name, - stages: formatStageDataForSubmission(this.stages, this.isEditing), - }; - if (this.isEditing) { - req = this.updateValueStream; - params = { - ...params, - id: this.initialData.id, - }; - } - this.isSubmitting = true; - const response = await req(params); - - if (this.hasFormErrors) { - this.isSubmitting = false; - this.showSubmitError = true; - return; - } + this.submitRequest() + .then(({ data: { id } }) => { + this.track('submit_form', { + label: this.isEditing ? 'edit_value_stream' : 'create_value_stream', + }); - this.track('submit_form', { - label: this.isEditing ? 'edit_value_stream' : 'create_value_stream', - }); + const redirectPath = mergeUrlParams({ value_stream_id: id }, this.vsaPath); + visitUrlWithAlerts(redirectPath, [this.submissionSuccessfulAlert]); + }) + .catch(({ response: { data } }) => { + this.isSubmitting = false; + this.showSubmitError = true; - const redirectPath = mergeUrlParams({ value_stream_id: response.data.id }, this.vsaPath); - visitUrlWithAlerts(redirectPath, [this.submissionSuccessfulAlert]); + const { + payload: { errors: { name, stages = {} } = {} }, + } = data; + this.setErrors({ + name, + stages: prepareStageErrors(this.submitParams.stages, stages), + }); + }); + }, + submitRequest() { + const { isEditing, namespaceRestApiRequestPath, initialData, submitParams } = this; + return isEditing + ? updateValueStream({ + namespacePath: namespaceRestApiRequestPath, + valueStreamId: initialData.id, + data: submitParams, + }) + : createValueStream(namespaceRestApiRequestPath, submitParams); }, stageGroupLabel(index) { return sprintf(this.$options.i18n.STAGE_INDEX, { index: index + 1 }); @@ -206,9 +207,8 @@ export default { }), ); }, - refreshErrors() { - const { defaultStageConfig, selectedPreset, createValueStreamErrors = {} } = this; - const { name = [], stages = [{}] } = createValueStreamErrors; + setErrors({ name = [], stages = [{}] }) { + const { defaultStageConfig, selectedPreset } = this; this.nameErrors = name; this.stageErrors = cloneDeep(stages) || initializeStageErrors(defaultStageConfig, selectedPreset); diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/utils.js b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/utils.js index 362b72ba17539954ca316976d3cbe9c58f3b7f4f..e9dd49ca1165cd51da5e2b69faeb89b056949fe2 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/utils.js +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/vsa_settings/utils.js @@ -1,5 +1,8 @@ import { isEqual, pick } from 'lodash'; -import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; +import { + convertObjectPropsToCamelCase, + convertObjectPropsToSnakeCase, +} from '~/lib/utils/common_utils'; import { isStartEvent, getAllowedEndEvents, @@ -225,6 +228,19 @@ const prepareDefaultStage = (defaultStageConfig, { name, ...rest }) => { }; }; +/** + * 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.map((_, index) => convertObjectPropsToCamelCase(errors[index]) || {}); + const generateHiddenDefaultStages = (defaultStageConfig, stageNames) => { // We use the stage name to check for any default stages that might be hidden // Currently the default stages can't be renamed diff --git a/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_select_spec.js b/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_select_spec.js index a0c05ecf58bcdc35bdff7f92b91b8f9b10e5eb4a..ddf3179a52adff86e5b2afe7f988f5187e3accac 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_select_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_select_spec.js @@ -36,7 +36,6 @@ describe('ValueStreamSelect', () => { new Vuex.Store({ state: { isDeletingValueStream: false, - createValueStreamErrors: {}, deleteValueStreamError: null, valueStreams: [], selectedValueStream: {}, diff --git a/ee/spec/frontend/analytics/cycle_analytics/store/actions/value_streams_spec.js b/ee/spec/frontend/analytics/cycle_analytics/store/actions/value_streams_spec.js index fb111a343875c766c3907e4881ed585c9c09c491..577074e9460d5c1c142c887267624a5b87cab85f 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/store/actions/value_streams_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/store/actions/value_streams_spec.js @@ -8,16 +8,8 @@ import { currentGroup } from 'jest/analytics/cycle_analytics/mock_data'; import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { allowedStages as stages, endpoints, valueStreams } from '../../mock_data'; -const mockStartEventIdentifier = 'issue_first_mentioned_in_commit'; -const mockEndEventIdentifier = 'issue_first_added_to_board'; -const mockEvents = { - startEventIdentifier: mockStartEventIdentifier, - endEventIdentifier: mockEndEventIdentifier, -}; - stages[0].hidden = true; const activeStages = stages.filter(({ hidden }) => !hidden); -const hiddenStage = stages[0]; const [selectedStage] = activeStages; const selectedStageSlug = selectedStage.slug; @@ -61,112 +53,6 @@ describe('Value Stream Analytics actions / value streams', () => { }); }); - describe('createValueStream', () => { - const payload = { - name: 'cool value stream', - stages: [ - { - ...selectedStage, - ...mockEvents, - id: null, - }, - { ...hiddenStage, ...mockEvents }, - ], - }; - - const createResp = { id: 'new value stream', is_custom: true, ...payload }; - - beforeEach(() => { - state = { currentGroup }; - }); - - describe('with no errors', () => { - beforeEach(() => { - mock.onPost(endpoints.valueStreamData).replyOnce(HTTP_STATUS_OK, createResp); - }); - - it(`commits ${types.REQUEST_CREATE_VALUE_STREAM}`, () => { - return testAction(actions.createValueStream, payload, state, [ - { - type: types.REQUEST_CREATE_VALUE_STREAM, - }, - ]); - }); - }); - - describe('with errors', () => { - const errors = { name: ['is taken'] }; - const message = { message: 'error' }; - const resp = { message, payload: { errors } }; - beforeEach(() => { - mock.onPost(endpoints.valueStreamData).replyOnce(HTTP_STATUS_NOT_FOUND, resp); - }); - - it(`commits the ${types.REQUEST_CREATE_VALUE_STREAM} and ${types.RECEIVE_CREATE_VALUE_STREAM_ERROR} actions `, () => { - return testAction( - actions.createValueStream, - payload, - state, - [ - { type: types.REQUEST_CREATE_VALUE_STREAM }, - { - type: types.RECEIVE_CREATE_VALUE_STREAM_ERROR, - payload: { message, data: payload, errors }, - }, - ], - [], - ); - }); - }); - }); - - describe('updateValueStream', () => { - const payload = { - name: 'cool value stream', - stages: [ - { - ...selectedStage, - ...mockEvents, - id: 'stage-1', - }, - { ...hiddenStage, ...mockEvents }, - ], - }; - const updateResp = { id: 'new value stream', is_custom: true, ...payload }; - - describe('with no errors', () => { - beforeEach(() => { - state = { currentGroup }; - mock.onPut(endpoints.valueStreamData).replyOnce(HTTP_STATUS_OK, updateResp); - }); - - it(`commits the ${types.REQUEST_UPDATE_VALUE_STREAM} mutation`, () => { - return testAction(actions.updateValueStream, payload, state, [ - { type: types.REQUEST_UPDATE_VALUE_STREAM }, - ]); - }); - }); - - describe('with errors', () => { - const errors = { name: ['is taken'] }; - const message = { message: 'error' }; - const resp = { message, payload: { errors } }; - beforeEach(() => { - mock.onPut(endpoints.valueStreamData).replyOnce(HTTP_STATUS_NOT_FOUND, resp); - }); - - it(`commits the ${types.REQUEST_UPDATE_VALUE_STREAM} and ${types.RECEIVE_UPDATE_VALUE_STREAM_ERROR} actions `, () => { - return testAction(actions.updateValueStream, payload, state, [ - { type: types.REQUEST_UPDATE_VALUE_STREAM }, - { - type: types.RECEIVE_UPDATE_VALUE_STREAM_ERROR, - payload: { message, data: payload, errors }, - }, - ]); - }); - }); - }); - describe('deleteValueStream', () => { const payload = 'my-fake-value-stream'; diff --git a/ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js b/ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js index 37ecab58eaf3f1d6d64b5f600d5ebc0a9ad37bf9..624d4f3112a580497f50ae4f2498106e7cae7aaa 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js @@ -37,55 +37,40 @@ describe('Value Stream Analytics mutations', () => { }); it.each` - mutation | stateKey | value - ${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]} - ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]} - ${types.REQUEST_VALUE_STREAMS} | ${'isLoadingValueStreams'} | ${true} - ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'isLoadingValueStreams'} | ${false} - ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true} - ${types.REQUEST_STAGE_DATA} | ${'selectedStageEvents'} | ${[]} - ${types.REQUEST_STAGE_DATA} | ${'pagination'} | ${{}} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]} - ${types.RECEIVE_STAGE_DATA_ERROR} | ${'pagination'} | ${{}} - ${types.REQUEST_VALUE_STREAM_DATA} | ${'isLoading'} | ${true} - ${types.RECEIVE_GROUP_STAGES_ERROR} | ${'stages'} | ${[]} - ${types.REQUEST_GROUP_STAGES} | ${'stages'} | ${[]} - ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} - ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} - ${types.REQUEST_CREATE_VALUE_STREAM} | ${'createValueStreamErrors'} | ${{}} - ${types.REQUEST_UPDATE_VALUE_STREAM} | ${'createValueStreamErrors'} | ${{}} - ${types.REQUEST_DELETE_VALUE_STREAM} | ${'isDeletingValueStream'} | ${true} - ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'isDeletingValueStream'} | ${false} - ${types.REQUEST_DELETE_VALUE_STREAM} | ${'deleteValueStreamError'} | ${null} - ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'deleteValueStreamError'} | ${null} - ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'selectedValueStream'} | ${null} - ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'stages'} | ${[]} - ${types.INITIALIZE_VALUE_STREAM_SUCCESS} | ${'isLoading'} | ${false} - ${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}} - ${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}} - ${types.REQUEST_GROUP_LABELS} | ${'defaultGroupLabels'} | ${[]} - ${types.RECEIVE_GROUP_LABELS_ERROR} | ${'defaultGroupLabels'} | ${[]} - ${types.SET_STAGE_EVENTS} | ${'formEvents'} | ${[]} + mutation | stateKey | value + ${types.REQUEST_VALUE_STREAMS} | ${'valueStreams'} | ${[]} + ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'valueStreams'} | ${[]} + ${types.REQUEST_VALUE_STREAMS} | ${'isLoadingValueStreams'} | ${true} + ${types.RECEIVE_VALUE_STREAMS_ERROR} | ${'isLoadingValueStreams'} | ${false} + ${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true} + ${types.REQUEST_STAGE_DATA} | ${'selectedStageEvents'} | ${[]} + ${types.REQUEST_STAGE_DATA} | ${'pagination'} | ${{}} + ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} + ${types.RECEIVE_STAGE_DATA_ERROR} | ${'selectedStageEvents'} | ${[]} + ${types.RECEIVE_STAGE_DATA_ERROR} | ${'pagination'} | ${{}} + ${types.REQUEST_VALUE_STREAM_DATA} | ${'isLoading'} | ${true} + ${types.RECEIVE_GROUP_STAGES_ERROR} | ${'stages'} | ${[]} + ${types.REQUEST_GROUP_STAGES} | ${'stages'} | ${[]} + ${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}} + ${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}} + ${types.REQUEST_DELETE_VALUE_STREAM} | ${'isDeletingValueStream'} | ${true} + ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'isDeletingValueStream'} | ${false} + ${types.REQUEST_DELETE_VALUE_STREAM} | ${'deleteValueStreamError'} | ${null} + ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'deleteValueStreamError'} | ${null} + ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'selectedValueStream'} | ${null} + ${types.RECEIVE_DELETE_VALUE_STREAM_SUCCESS} | ${'stages'} | ${[]} + ${types.INITIALIZE_VALUE_STREAM_SUCCESS} | ${'isLoading'} | ${false} + ${types.REQUEST_STAGE_COUNTS} | ${'stageCounts'} | ${{}} + ${types.RECEIVE_STAGE_COUNTS_ERROR} | ${'stageCounts'} | ${{}} + ${types.REQUEST_GROUP_LABELS} | ${'defaultGroupLabels'} | ${[]} + ${types.RECEIVE_GROUP_LABELS_ERROR} | ${'defaultGroupLabels'} | ${[]} + ${types.SET_STAGE_EVENTS} | ${'formEvents'} | ${[]} `('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => { mutations[mutation](state); expect(state[stateKey]).toEqual(value); }); - const valueStreamErrors = { - data: { stages }, - errors: { - name: ['is required'], - stages: { 1: { name: "Can't be blank" } }, - }, - }; - - const expectedValueStreamErrors = { - name: ['is required'], - stages: [{}, { name: "Can't be blank" }, {}, {}, {}, {}, {}, {}], - }; - const pagination = { page: 10, hasNextPage: true, sort: null, direction: null }; it.each` @@ -94,8 +79,6 @@ describe('Value Stream Analytics mutations', () => { ${types.SET_DATE_RANGE} | ${{ createdAfter, createdBefore }} | ${{ createdAfter, createdBefore }} ${types.SET_PREDEFINED_DATE_RANGE} | ${predefinedDateRange} | ${{ predefinedDateRange }} ${types.SET_SELECTED_STAGE} | ${{ id: 'first-stage' }} | ${{ selectedStage: { id: 'first-stage' } }} - ${types.RECEIVE_CREATE_VALUE_STREAM_ERROR} | ${valueStreamErrors} | ${{ createValueStreamErrors: expectedValueStreamErrors }} - ${types.RECEIVE_UPDATE_VALUE_STREAM_ERROR} | ${valueStreamErrors} | ${{ createValueStreamErrors: expectedValueStreamErrors }} ${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: {} }} diff --git a/ee/spec/frontend/analytics/cycle_analytics/utils_spec.js b/ee/spec/frontend/analytics/cycle_analytics/utils_spec.js index a92627ec7282ac5760cb3347e6fc7c406527e54e..bd5dad9a520fd85eaf544b399cb29ee3f9152fe6 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/utils_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/utils_spec.js @@ -14,7 +14,6 @@ import { flattenTaskByTypeSeries, orderByDate, toggleSelectedLabel, - prepareStageErrors, formatMedianValues, generateFilterTextDescription, groupDurationsByDay, @@ -330,29 +329,6 @@ 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).toHaveLength(stages.length); - }); - - it('returns an empty object for each stage if there are no errors', () => { - const res = prepareStageErrors(stages, {}); - expect(res).toEqual([{}, {}, {}]); - }); - }); - describe('flattenTaskByTypeSeries', () => { const dummySeries = Object.fromEntries([ ['2019-01-16', 40], diff --git a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_spec.js b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_spec.js index 93490f4a92574fea70b6a7bcbc15292ee9c24413..97b6364b5f4fbb242097e9cb4f789e5689907a5c 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content_spec.js @@ -2,8 +2,11 @@ import { GlAlert } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; import CrudComponent from '~/vue_shared/components/crud_component.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { PRESET_OPTIONS_BLANK, PRESET_OPTIONS_DEFAULT, @@ -12,6 +15,7 @@ import CustomStageFields from 'ee/analytics/cycle_analytics/vsa_settings/compone import DefaultStageFields from 'ee/analytics/cycle_analytics/vsa_settings/components/default_stage_fields.vue'; import ValueStreamFormContent from 'ee/analytics/cycle_analytics/vsa_settings/components/value_stream_form_content.vue'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; +import { HTTP_STATUS_OK, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; import { visitUrlWithAlerts } from '~/lib/utils/url_utility'; import { convertObjectPropsToCamelCase, @@ -23,6 +27,7 @@ import { defaultStageConfig, rawCustomStage, groupLabels as defaultGroupLabels, + endpoints, } from '../../mock_data'; jest.mock('~/lib/utils/url_utility', () => ({ @@ -33,12 +38,12 @@ jest.mock('~/lib/utils/url_utility', () => ({ Vue.use(Vuex); describe('ValueStreamFormContent', () => { + let mock; let wrapper = null; let trackingSpy = null; const mockValueStream = { id: 13 }; - const createValueStreamMock = jest.fn(() => Promise.resolve({ data: mockValueStream })); - const updateValueStreamMock = jest.fn(() => Promise.resolve({ data: mockValueStream })); + const namespacePath = 'fake/group/path'; const streamName = 'Cool stream'; const formSubmissionErrors = { name: ['has already been taken'], @@ -50,8 +55,8 @@ describe('ValueStreamFormContent', () => { }; const initialData = { + ...mockValueStream, stages: [convertObjectPropsToCamelCase(rawCustomStage)], - id: 1337, name: 'Editable value stream', }; @@ -60,18 +65,11 @@ describe('ValueStreamFormContent', () => { state: { formEvents, defaultGroupLabels, - createValueStreamErrors: {}, selectedValueStream: undefined, ...stateOverrides, }, - mutations: { - setCreateValueStreamErrors(state, value) { - state.createValueStreamErrors = value; - }, - }, - actions: { - createValueStream: createValueStreamMock, - updateValueStream: updateValueStreamMock, + getters: { + namespaceRestApiRequestPath: () => namespacePath, }, }); @@ -115,6 +113,14 @@ describe('ValueStreamFormContent', () => { findPresetSelector().vm.$emit('input', PRESET_OPTIONS_DEFAULT); const changeToCustomStages = () => findPresetSelector().vm.$emit('input', PRESET_OPTIONS_BLANK); + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + describe('when creating value stream', () => { beforeEach(() => { wrapper = createComponent({ state: { defaultGroupLabels: null } }); @@ -243,47 +249,6 @@ describe('ValueStreamFormContent', () => { }); }); - describe('initial form stage errors', () => { - const createValueStreamErrors = { - stages: [ - { - name: ['Name field is required'], - startEventIdentifier: ['Start event is required'], - }, - ], - }; - - beforeEach(() => { - wrapper = createComponent({ - state: { createValueStreamErrors }, - }); - }); - - it('renders errors for a default stage field', () => { - expect(findDefaultStages().at(0).props().errors).toEqual(createValueStreamErrors.stages[0]); - }); - }); - - describe('initial form name errors', () => { - const nameError = 'Name field required'; - - beforeEach(() => { - wrapper = createComponent({ - state: { - createValueStreamErrors: { name: [nameError] }, - }, - }); - }); - - it('sets the feedback for the name form group', () => { - expect(findNameFormGroup().attributes('invalid-feedback')).toBe(nameError); - }); - - it('sets the state for the name input', () => { - expect(findNameInput().props().state).toBe(false); - }); - }); - describe('with valid fields', () => { beforeEach(() => { trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); @@ -295,14 +260,18 @@ describe('ValueStreamFormContent', () => { describe('form submitted successfully', () => { beforeEach(async () => { + mock.onPost(endpoints.valueStreamData).replyOnce(HTTP_STATUS_OK, mockValueStream); wrapper = createComponent(); await findNameInput().vm.$emit('input', streamName); clickSubmit(); + + await waitForPromises(); }); - it('calls the "createValueStream" event when submitted', () => { - expect(createValueStreamMock).toHaveBeenCalledWith(expect.any(Object), { + it('sends a create request', () => { + expect(mock.history.post).toHaveLength(1); + expect(JSON.parse(mock.history.post[0].data)).toEqual({ name: streamName, stages: [ { @@ -344,16 +313,20 @@ describe('ValueStreamFormContent', () => { describe('form submission fails', () => { beforeEach(async () => { + mock.onPost(endpoints.valueStreamData).replyOnce(HTTP_STATUS_NOT_FOUND, { + payload: { errors: formSubmissionErrors }, + }); + wrapper = createComponent(); await findNameInput().vm.$emit('input', streamName); clickSubmit(); - wrapper.vm.$store.commit('setCreateValueStreamErrors', formSubmissionErrors); + await waitForPromises(); }); - it('calls the createValueStream action', () => { - expect(createValueStreamMock).toHaveBeenCalled(); + it('sends a create request', () => { + expect(mock.history.post).toHaveLength(1); }); it('does not clear the name field', () => { @@ -523,6 +496,8 @@ describe('ValueStreamFormContent', () => { describe('form submitted successfully', () => { beforeEach(() => { + mock.onPut(endpoints.valueStreamData).replyOnce(HTTP_STATUS_OK, mockValueStream); + wrapper = createComponent({ props: { initialData, @@ -534,11 +509,16 @@ describe('ValueStreamFormContent', () => { }); clickSubmit(); + return waitForPromises(); }); - it('calls the "updateValueStreamMock" event when submitted', () => { - expect(updateValueStreamMock).toHaveBeenCalledWith(expect.any(Object), { - ...initialData, + it('sends an update request', () => { + expect(mock.history.put).toHaveLength(1); + expect(mock.history.put[0].url).toBe( + '/fake/group/path/-/analytics/value_stream_analytics/value_streams/13', + ); + expect(JSON.parse(mock.history.put[0].data)).toEqual({ + name: initialData.name, stages: initialData.stages.map((stage) => convertObjectPropsToSnakeCase(stage, { deep: true }), ), @@ -568,6 +548,10 @@ describe('ValueStreamFormContent', () => { describe('form submission fails', () => { beforeEach(() => { + mock.onPut(endpoints.valueStreamData).replyOnce(HTTP_STATUS_NOT_FOUND, { + payload: { errors: formSubmissionErrors }, + }); + wrapper = createComponent({ props: { initialData, @@ -579,11 +563,11 @@ describe('ValueStreamFormContent', () => { }); clickSubmit(); - wrapper.vm.$store.commit('setCreateValueStreamErrors', formSubmissionErrors); + return waitForPromises(); }); - it('calls the updateValueStreamMock action', () => { - expect(updateValueStreamMock).toHaveBeenCalled(); + it('sends an update request', () => { + expect(mock.history.put).toHaveLength(1); }); it('does not clear the name field', () => { diff --git a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/utils_spec.js b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/utils_spec.js index acca6a1571c24d00fea0e6962d895629e4dc9ff5..5716d97e14cd6110cd689a9c4ffe8d0910664985 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/utils_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/vsa_settings/utils_spec.js @@ -10,6 +10,7 @@ import { formatStageDataForSubmission, generateInitialStageData, cleanStageName, + prepareStageErrors, } from 'ee/analytics/cycle_analytics/vsa_settings/utils'; import { labelStartEvent, labelEndEvent } from 'ee_jest/analytics/cycle_analytics/mock_data'; @@ -301,3 +302,21 @@ describe('generateInitialStageData', () => { }); }); }); + +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', () => { + expect(prepareStageErrors(stages, stageErrors)).toEqual([{}, nameError, {}]); + }); + + it('returns the same number of error objects as stages', () => { + expect(prepareStageErrors(stages, stageErrors)).toHaveLength(stages.length); + }); + + it('returns an empty object for each stage if there are no errors', () => { + expect(prepareStageErrors(stages, {})).toEqual([{}, {}, {}]); + }); +});