Commit f8e8d27a authored by Martin Wortschack's avatar Martin Wortschack

Merge branch...

Merge branch '214893-value-stream-analytics-move-custom-stages-form-to-separate-module' into 'master'

Move custom stages form vuex to separate module"

See merge request gitlab-org/gitlab!30684
parents 61506593 f275366f
......@@ -57,22 +57,24 @@ export default {
'isLoading',
'isLoadingStage',
'isEmptyStage',
'isSavingCustomStage',
'isCreatingCustomStage',
'isEditingCustomStage',
'selectedGroup',
'selectedProjects',
'selectedStage',
'stages',
'summary',
'currentStageEvents',
'customStageFormEvents',
'errorCode',
'startDate',
'endDate',
'medians',
'customStageFormErrors',
'customStageFormInitialData',
]),
...mapState('customStages', [
'isSavingCustomStage',
'isCreatingCustomStage',
'isEditingCustomStage',
'formEvents',
'formErrors',
'formInitialData',
]),
...mapGetters([
'hasNoAccessError',
......@@ -81,8 +83,8 @@ export default {
'selectedProjectIds',
'enableCustomOrdering',
'cycleAnalyticsRequestParams',
'customStageFormActive',
]),
...mapGetters('customStages', ['customStageFormActive']),
shouldRenderEmptyState() {
return !this.selectedGroup;
},
......@@ -98,6 +100,9 @@ export default {
isLoadingTypeOfWork() {
return this.isLoadingTasksByTypeChartTopLabels || this.isLoadingTasksByTypeChart;
},
isUpdatingCustomStage() {
return this.isEditingCustomStage && this.isSavingCustomStage;
},
hasDateRangeSet() {
return this.startDate && this.endDate;
},
......@@ -126,18 +131,20 @@ export default {
'setSelectedGroup',
'setSelectedProjects',
'setSelectedStage',
'hideCustomStageForm',
'showCustomStageForm',
'showEditCustomStageForm',
'setDateRange',
'createCustomStage',
'updateStage',
'removeStage',
'setFeatureFlags',
'clearCustomStageFormErrors',
'updateStage',
'reorderStage',
]),
...mapActions('customStages', [
'hideForm',
'showCreateForm',
'showEditForm',
'createStage',
'clearFormErrors',
]),
onGroupSelect(group) {
this.setSelectedGroup(group);
this.fetchCycleAnalyticsData();
......@@ -147,18 +154,18 @@ export default {
this.fetchCycleAnalyticsData();
},
onStageSelect(stage) {
this.hideCustomStageForm();
this.hideForm();
this.setSelectedStage(stage);
this.fetchStageData(this.selectedStage.slug);
},
onShowAddStageForm() {
this.showCustomStageForm();
this.showCreateForm();
},
onShowEditStageForm(initData = {}) {
this.showEditCustomStageForm(initData);
this.showEditForm(initData);
},
onCreateCustomStage(data) {
this.createCustomStage(data);
this.createStage(data);
},
onUpdateCustomStage(data) {
this.updateStage(data);
......@@ -288,15 +295,17 @@ export default {
/>
</template>
<template v-if="customStageFormActive" #content>
<gl-loading-icon v-if="isUpdatingCustomStage" class="mt-4" size="md" />
<custom-stage-form
:events="customStageFormEvents"
v-else
:events="formEvents"
:is-saving-custom-stage="isSavingCustomStage"
:initial-fields="customStageFormInitialData"
:initial-fields="formInitialData"
:is-editing-custom-stage="isEditingCustomStage"
:errors="customStageFormErrors"
:errors="formErrors"
@createStage="onCreateCustomStage"
@updateStage="onUpdateCustomStage"
@clearErrors="$emit('clearCustomStageFormErrors')"
@clearErrors="$emit('clearFormErrors')"
/>
</template>
</stage-table>
......
......@@ -3,15 +3,7 @@ import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types';
import { removeFlash, handleErrorOrRethrow } from '../utils';
const isStageNameExistsError = ({ status, errors }) => {
const ERROR_NAME_RESERVED = 'is reserved';
if (status === httpStatus.UNPROCESSABLE_ENTITY) {
if (errors?.name?.includes(ERROR_NAME_RESERVED)) return true;
}
return false;
};
import { removeFlash, handleErrorOrRethrow, isStageNameExistsError } from '../utils';
export const setFeatureFlags = ({ commit }, featureFlags) =>
commit(types.SET_FEATURE_FLAGS, featureFlags);
......@@ -104,10 +96,10 @@ export const receiveCycleAnalyticsDataSuccess = ({ commit, dispatch }) => {
};
export const receiveCycleAnalyticsDataError = ({ commit }, { response }) => {
const { status } = response;
const { status = null } = response; // non api errors thrown won't have a status field
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR, status);
if (status !== httpStatus.FORBIDDEN)
if (!status || status !== httpStatus.FORBIDDEN)
createFlash(__('There was an error while fetching value stream analytics data.'));
};
......@@ -122,56 +114,30 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
.catch(error => dispatch('receiveCycleAnalyticsDataError', error));
};
export const hideCustomStageForm = ({ commit }) => {
commit(types.HIDE_CUSTOM_STAGE_FORM);
removeFlash();
};
export const requestGroupStages = ({ commit }) => commit(types.REQUEST_GROUP_STAGES);
export const showCustomStageForm = ({ commit }) => {
commit(types.SHOW_CUSTOM_STAGE_FORM);
removeFlash();
};
export const showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {}) => {
const {
id = null,
name = null,
startEventIdentifier = null,
startEventLabel: { id: startEventLabelId = null } = {},
endEventIdentifier = null,
endEventLabel: { id: endEventLabelId = null } = {},
} = selectedStage;
commit(types.SHOW_EDIT_CUSTOM_STAGE_FORM, {
id,
name,
startEventIdentifier,
startEventLabelId,
endEventIdentifier,
endEventLabelId,
});
dispatch('setSelectedStage', selectedStage);
removeFlash();
export const receiveGroupStagesError = ({ commit }, error) => {
commit(types.RECEIVE_GROUP_STAGES_ERROR, error);
createFlash(__('There was an error fetching value stream analytics stages.'));
};
export const requestGroupStagesAndEvents = ({ commit }) =>
commit(types.REQUEST_GROUP_STAGES_AND_EVENTS);
export const setDefaultSelectedStage = ({ dispatch, getters }) => {
const { activeStages = [] } = getters;
if (activeStages?.length) {
const [firstActiveStage] = activeStages;
return Promise.all([
dispatch('setSelectedStage', firstActiveStage),
dispatch('fetchStageData', firstActiveStage.slug),
]);
}
export const receiveGroupStagesAndEventsError = ({ commit }, error) => {
commit(types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR, error);
createFlash(__('There was an error fetching value stream analytics stages.'));
createFlash(__('There was an error while fetching value stream analytics data.'));
return Promise.resolve();
};
export const receiveGroupStagesAndEventsSuccess = ({ state, commit, dispatch }, data) => {
commit(types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS, data);
const { stages = [] } = state;
if (stages && stages.length) {
const [firstStage] = stages;
dispatch('setSelectedStage', firstStage);
dispatch('fetchStageData', firstStage.slug);
} else {
createFlash(__('There was an error while fetching value stream analytics data.'));
}
export const receiveGroupStagesSuccess = ({ commit, dispatch }, stages) => {
commit(types.RECEIVE_GROUP_STAGES_SUCCESS, stages);
return dispatch('setDefaultSelectedStage');
};
export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
......@@ -182,85 +148,39 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
const {
cycleAnalyticsRequestParams: { created_after, project_ids },
} = getters;
dispatch('requestGroupStagesAndEvents');
dispatch('requestGroupStages');
dispatch('customStages/setStageEvents', []);
return Api.cycleAnalyticsGroupStagesAndEvents(fullPath, {
start_date: created_after,
project_ids,
})
.then(({ data }) => dispatch('receiveGroupStagesAndEventsSuccess', data))
.then(({ data: { stages = [], events = [] } }) => {
dispatch('receiveGroupStagesSuccess', stages);
dispatch('customStages/setStageEvents', events);
})
.catch(error =>
handleErrorOrRethrow({
error,
action: () => dispatch('receiveGroupStagesAndEventsError', error),
action: () => dispatch('receiveGroupStagesError', error),
}),
);
};
export const clearCustomStageFormErrors = ({ commit }) => {
commit(types.CLEAR_CUSTOM_STAGE_FORM_ERRORS);
removeFlash();
};
export const requestCreateCustomStage = ({ commit }) => commit(types.REQUEST_CREATE_CUSTOM_STAGE);
export const receiveCreateCustomStageSuccess = ({ commit, dispatch }, { data: { title } }) => {
commit(types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS);
createFlash(sprintf(__(`Your custom stage '%{title}' was created`), { title }), 'notice');
export const requestUpdateStage = ({ commit }) => commit(types.REQUEST_UPDATE_STAGE);
export const receiveUpdateStageSuccess = ({ commit, dispatch }, updatedData) => {
commit(types.RECEIVE_UPDATE_STAGE_SUCCESS);
createFlash(__('Stage data updated'), 'notice');
return Promise.resolve()
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('customStages/showEditForm', updatedData))
.catch(() => {
createFlash(__('There was a problem refreshing the data, please try again'));
});
};
export const receiveCreateCustomStageError = (
{ commit },
{ status = 400, errors = {}, data = {} } = {},
) => {
commit(types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR, { errors });
const { name = null } = data;
const flashMessage =
name && isStageNameExistsError({ status, errors })
? sprintf(__(`'%{name}' stage already exists`), { name })
: __('There was a problem saving your custom stage, please try again');
createFlash(flashMessage);
};
export const createCustomStage = ({ dispatch, state }, data) => {
const {
selectedGroup: { fullPath },
} = state;
dispatch('requestCreateCustomStage');
return Api.cycleAnalyticsCreateStage(fullPath, data)
.then(response => {
const { status, data: responseData } = response;
return dispatch('receiveCreateCustomStageSuccess', { status, data: responseData });
})
.catch(({ response } = {}) => {
const { data: { message, errors } = null, status = 400 } = response;
dispatch('receiveCreateCustomStageError', { data, message, errors, status });
});
};
export const requestUpdateStage = ({ commit }) => commit(types.REQUEST_UPDATE_STAGE);
export const receiveUpdateStageSuccess = ({ commit, dispatch }, updatedData) => {
commit(types.RECEIVE_UPDATE_STAGE_SUCCESS);
createFlash(__('Stage data updated'), 'notice');
return Promise.all([
dispatch('fetchGroupStagesAndEvents'),
dispatch('setSelectedStage', updatedData),
]).catch(() => {
createFlash(__('There was a problem refreshing the data, please try again'));
});
};
export const receiveUpdateStageError = (
{ commit },
{ commit, dispatch },
{ status, responseData: { errors = null } = {}, data = {} },
) => {
commit(types.RECEIVE_UPDATE_STAGE_ERROR, { errors, data });
......@@ -272,6 +192,7 @@ export const receiveUpdateStageError = (
: __('There was a problem saving your custom stage, please try again');
createFlash(__(message));
return dispatch('customStages/setStageFormErrors', errors);
};
export const updateStage = ({ dispatch, state }, { id, ...rest }) => {
......@@ -280,6 +201,7 @@ export const updateStage = ({ dispatch, state }, { id, ...rest }) => {
} = state;
dispatch('requestUpdateStage');
dispatch('customStages/setSavingCustomStage');
return Api.cycleAnalyticsUpdateStage(id, fullPath, { ...rest })
.then(({ data }) => dispatch('receiveUpdateStageSuccess', data))
......@@ -292,7 +214,7 @@ export const requestRemoveStage = ({ commit }) => commit(types.REQUEST_REMOVE_ST
export const receiveRemoveStageSuccess = ({ commit, dispatch }) => {
commit(types.RECEIVE_REMOVE_STAGE_RESPONSE);
createFlash(__('Stage removed'), 'notice');
dispatch('fetchCycleAnalyticsData');
return dispatch('fetchCycleAnalyticsData');
};
export const receiveRemoveStageError = ({ commit }) => {
......
......@@ -4,6 +4,7 @@ import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
import customStages from './modules/custom_stages/index';
import durationChart from './modules/duration_chart/index';
import typeOfWork from './modules/type_of_work/index';
......@@ -15,5 +16,5 @@ export default () =>
getters,
mutations,
state,
modules: { durationChart, typeOfWork },
modules: { customStages, durationChart, typeOfWork },
});
import Api from 'ee/api';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import httpStatusCodes from '~/lib/utils/http_status';
import * as types from './mutation_types';
import { removeFlash, isStageNameExistsError } from '../../../utils';
export const setStageEvents = ({ commit }, data) => commit(types.SET_STAGE_EVENTS, data);
export const setStageFormErrors = ({ commit }, errors) =>
commit(types.SET_STAGE_FORM_ERRORS, errors);
export const hideForm = ({ commit }) => {
commit(types.HIDE_FORM);
removeFlash();
};
export const showCreateForm = ({ commit }) => {
commit(types.SHOW_CREATE_FORM);
removeFlash();
};
export const showEditForm = ({ commit, dispatch }, selectedStage = {}) => {
commit(types.SET_FORM_INITIAL_DATA, selectedStage);
commit(types.SHOW_EDIT_FORM);
dispatch('setSelectedStage', selectedStage, { root: true });
dispatch('clearSavingCustomStage');
removeFlash();
};
export const clearFormErrors = ({ commit }) => {
commit(types.CLEAR_FORM_ERRORS);
removeFlash();
};
export const setSavingCustomStage = ({ commit }) => commit(types.SET_SAVING_CUSTOM_STAGE);
export const clearSavingCustomStage = ({ commit }) => commit(types.CLEAR_SAVING_CUSTOM_STAGE);
export const receiveCreateStageSuccess = ({ commit, dispatch }, { data: { title } }) => {
commit(types.RECEIVE_CREATE_STAGE_SUCCESS);
createFlash(sprintf(__(`Your custom stage '%{title}' was created`), { title }), 'notice');
return Promise.resolve()
.then(() => dispatch('fetchGroupStagesAndEvents', null, { root: true }))
.then(() => dispatch('clearSavingCustomStage'))
.catch(() => {
createFlash(__('There was a problem refreshing the data, please try again'));
});
};
export const receiveCreateStageError = (
{ commit, dispatch },
{ status = httpStatusCodes.BAD_REQUEST, errors = {}, data = {} } = {},
) => {
commit(types.RECEIVE_CREATE_STAGE_ERROR);
const { name = null } = data;
const flashMessage =
name && isStageNameExistsError({ status, errors })
? sprintf(__(`'%{name}' stage already exists`), { name })
: __('There was a problem saving your custom stage, please try again');
createFlash(flashMessage);
return dispatch('setStageFormErrors', errors);
};
export const createStage = ({ dispatch, rootState }, data) => {
const {
selectedGroup: { fullPath },
} = rootState;
dispatch('clearFormErrors');
dispatch('setSavingCustomStage');
return Api.cycleAnalyticsCreateStage(fullPath, data)
.then(response => {
const { status, data: responseData } = response;
return dispatch('receiveCreateStageSuccess', { status, data: responseData });
})
.catch(({ response } = {}) => {
const { data: { message, errors } = null, status = httpStatusCodes.BAD_REQUEST } = response;
dispatch('receiveCreateStageError', { data, message, errors, status });
});
};
// eslint-disable-next-line import/prefer-default-export
export const customStageFormActive = ({ isCreatingCustomStage, isEditingCustomStage }) =>
Boolean(isCreatingCustomStage || isEditingCustomStage);
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
import * as actions from './actions';
export default {
namespaced: true,
state,
mutations,
getters,
actions,
};
export const SET_STAGE_EVENTS = 'SET_STAGE_EVENTS';
export const SET_STAGE_FORM_ERRORS = 'SET_STAGE_FORM_ERRORS';
export const SET_FORM_INITIAL_DATA = 'SET_FORM_INITIAL_DATA';
export const SET_SAVING_CUSTOM_STAGE = 'SET_SAVING_CUSTOM_STAGE';
export const CLEAR_SAVING_CUSTOM_STAGE = 'CLEAR_SAVING_CUSTOM_STAGE';
export const HIDE_FORM = 'SHOW_FORM';
export const SHOW_CREATE_FORM = 'SHOW_CREATE_FORM';
export const SHOW_EDIT_FORM = 'SHOW_EDIT_FORM';
export const CLEAR_FORM_ERRORS = 'CLEAR_FORM_ERRORS';
export const RECEIVE_CREATE_STAGE_SUCCESS = 'RECEIVE_CREATE_STAGE_SUCCESS';
export const RECEIVE_CREATE_STAGE_ERROR = 'RECEIVE_CREATE_STAGE_ERROR';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
import { transformRawStages } from '../../../utils';
const extractFormFields = (rawStage = {}) => {
const [
{
id = null,
name = null,
startEventIdentifier = null,
startEventLabel: { id: startEventLabelId = null } = {},
endEventIdentifier = null,
endEventLabel: { id: endEventLabelId = null } = {},
},
] = transformRawStages([rawStage]);
return {
id,
name,
startEventIdentifier,
startEventLabelId,
endEventIdentifier,
endEventLabelId,
};
};
export default {
[types.SET_STAGE_EVENTS](state, data = []) {
state.formEvents = data.map(ev => convertObjectPropsToCamelCase(ev, { deep: true }));
},
[types.SET_STAGE_FORM_ERRORS](state, errors) {
state.formErrors = convertObjectPropsToCamelCase(errors, { deep: true });
},
[types.SET_FORM_INITIAL_DATA](state, rawStageData = null) {
state.formInitialData = extractFormFields(rawStageData);
},
[types.SET_SAVING_CUSTOM_STAGE](state) {
state.isSavingCustomStage = true;
},
[types.CLEAR_SAVING_CUSTOM_STAGE](state) {
state.isSavingCustomStage = false;
},
[types.SHOW_CREATE_FORM](state) {
state.isEditingCustomStage = false;
state.isCreatingCustomStage = true;
state.formInitialData = null;
state.formErrors = null;
},
[types.SHOW_EDIT_FORM](state) {
state.isCreatingCustomStage = false;
state.isEditingCustomStage = true;
state.formErrors = null;
},
[types.HIDE_FORM](state) {
state.isEditingCustomStage = false;
state.isCreatingCustomStage = false;
state.formInitialData = null;
state.formErrors = null;
},
[types.CLEAR_FORM_ERRORS](state) {
state.formErrors = null;
},
[types.RECEIVE_CREATE_STAGE_ERROR](state) {
state.isSavingCustomStage = false;
},
[types.RECEIVE_CREATE_STAGE_SUCCESS](state) {
state.formErrors = null;
state.formInitialData = null;
},
};
export default () => ({
isSavingCustomStage: false,
isCreatingCustomStage: false,
isEditingCustomStage: false,
formEvents: [],
formErrors: null,
formInitialData: null,
});
......@@ -17,18 +17,9 @@ export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS';
export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS';
export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR';
export const HIDE_CUSTOM_STAGE_FORM = 'HIDE_CUSTOM_STAGE_FORM';
export const SHOW_CUSTOM_STAGE_FORM = 'SHOW_CUSTOM_STAGE_FORM';
export const SHOW_EDIT_CUSTOM_STAGE_FORM = 'SHOW_EDIT_CUSTOM_STAGE_FORM';
export const CLEAR_CUSTOM_STAGE_FORM_ERRORS = 'CLEAR_CUSTOM_STAGE_FORM_ERRORS';
export const REQUEST_GROUP_STAGES_AND_EVENTS = 'REQUEST_GROUP_STAGES_AND_EVENTS';
export const RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS = 'RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS';
export const RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR = 'RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR';
export const REQUEST_CREATE_CUSTOM_STAGE = 'REQUEST_CREATE_CUSTOM_STAGE';
export const RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS = 'RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS';
export const RECEIVE_CREATE_CUSTOM_STAGE_ERROR = 'RECEIVE_CREATE_CUSTOM_STAGE_ERROR';
export const REQUEST_GROUP_STAGES = 'REQUEST_GROUP_STAGES';
export const RECEIVE_GROUP_STAGES_SUCCESS = 'RECEIVE_GROUP_STAGES_SUCCESS';
export const RECEIVE_GROUP_STAGES_ERROR = 'RECEIVE_GROUP_STAGES_ERROR';
export const REQUEST_UPDATE_STAGE = 'REQUEST_UPDATE_STAGE';
export const RECEIVE_UPDATE_STAGE_SUCCESS = 'RECEIVE_UPDATE_STAGE_SUCCESS';
......
......@@ -22,8 +22,6 @@ export default {
},
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.isCreatingCustomStage = false;
state.isEditingCustomStage = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state) {
state.errorCode = null;
......@@ -63,73 +61,23 @@ export default {
[types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
state.medians = {};
},
[types.SHOW_CUSTOM_STAGE_FORM](state) {
state.isCreatingCustomStage = true;
state.isEditingCustomStage = false;
state.customStageFormInitialData = null;
state.customStageFormErrors = null;
},
[types.SHOW_EDIT_CUSTOM_STAGE_FORM](state, initialData) {
state.isEditingCustomStage = true;
state.isCreatingCustomStage = false;
state.customStageFormInitialData = initialData;
state.customStageFormErrors = null;
},
[types.HIDE_CUSTOM_STAGE_FORM](state) {
state.isEditingCustomStage = false;
state.isCreatingCustomStage = false;
state.customStageFormInitialData = null;
state.customStageFormErrors = null;
},
[types.CLEAR_CUSTOM_STAGE_FORM_ERRORS](state) {
state.customStageFormErrors = null;
},
[types.REQUEST_GROUP_STAGES_AND_EVENTS](state) {
[types.REQUEST_GROUP_STAGES](state) {
state.stages = [];
state.customStageFormEvents = [];
},
[types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR](state) {
[types.RECEIVE_GROUP_STAGES_ERROR](state) {
state.stages = [];
state.customStageFormEvents = [];
},
[types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS](state, data) {
const { events = [], stages = [] } = data;
[types.RECEIVE_GROUP_STAGES_SUCCESS](state, stages) {
state.stages = transformRawStages(stages);
state.customStageFormEvents = events.map(ev =>
convertObjectPropsToCamelCase(ev, { deep: true }),
);
},
[types.REQUEST_CREATE_CUSTOM_STAGE](state) {
state.isSavingCustomStage = true;
state.customStageFormErrors = {};
},
[types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR](state, { errors = null } = {}) {
state.isSavingCustomStage = false;
state.customStageFormErrors = convertObjectPropsToCamelCase(errors, { deep: true });
},
[types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS](state) {
state.isSavingCustomStage = false;
state.customStageFormErrors = null;
state.customStageFormInitialData = null;
},
[types.REQUEST_UPDATE_STAGE](state) {
state.isLoading = true;
state.isSavingCustomStage = true;
state.customStageFormErrors = null;
},
[types.RECEIVE_UPDATE_STAGE_SUCCESS](state) {
state.isLoading = false;
state.isSavingCustomStage = false;
state.isEditingCustomStage = false;
state.customStageFormErrors = null;
state.customStageFormInitialData = null;
},
[types.RECEIVE_UPDATE_STAGE_ERROR](state, { errors = null, data } = {}) {
[types.RECEIVE_UPDATE_STAGE_ERROR](state) {
state.isLoading = false;
state.isSavingCustomStage = false;
state.customStageFormErrors = convertObjectPropsToCamelCase(errors, { deep: true });
state.customStageFormInitialData = convertObjectPropsToCamelCase(data, { deep: true });
},
[types.REQUEST_REMOVE_STAGE](state) {
state.isLoading = true;
......
......@@ -10,9 +10,6 @@ export default () => ({
isEmptyStage: false,
errorCode: null,
isSavingCustomStage: false,
isCreatingCustomStage: false,
isEditingCustomStage: false,
isSavingStageOrder: false,
errorSavingStageOrder: false,
......@@ -25,8 +22,4 @@ export default () => ({
stages: [],
summary: [],
medians: {},
customStageFormEvents: [],
customStageFormErrors: null,
customStageFormInitialData: null,
});
......@@ -18,6 +18,7 @@ import { STAGE_NAME } from './constants';
import { toYmd } from '../shared/utils';
const EVENT_TYPE_LABEL = 'label';
const ERROR_NAME_RESERVED = 'is reserved';
export const removeFlash = (type = 'alert') => {
const flashEl = document.querySelector(`.flash-${type}`);
......@@ -337,3 +338,6 @@ export const handleErrorOrRethrow = ({ action, error }) => {
}
action();
};
export const isStageNameExistsError = ({ status, errors }) =>
status === httpStatus.UNPROCESSABLE_ENTITY && errors?.name?.includes(ERROR_NAME_RESERVED);
......@@ -81,9 +81,10 @@ function createComponent({
...selectedGroup,
});
comp.vm.$store.dispatch('receiveGroupStagesAndEventsSuccess', {
...mockData.customizableStagesAndEvents,
});
comp.vm.$store.dispatch(
'receiveGroupStagesSuccess',
mockData.customizableStagesAndEvents.stages,
);
comp.vm.$store.dispatch('receiveStageDataSuccess', mockData.issueEvents);
}
......
......@@ -60,7 +60,7 @@ export const customizableStagesAndEvents = getJSONFixture(
const dummyState = {};
// prepare the raw stage data for our components
mutations[types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS](dummyState, customizableStagesAndEvents);
mutations[types.RECEIVE_GROUP_STAGES_SUCCESS](dummyState, customizableStagesAndEvents.stages);
export const issueStage = getStageByTitle(dummyState.stages, 'issue');
export const planStage = getStageByTitle(dummyState.stages, 'plan');
......
......@@ -4,6 +4,8 @@ import testAction from 'helpers/vuex_action_helper';
import * as getters from 'ee/analytics/cycle_analytics/store/getters';
import * as actions from 'ee/analytics/cycle_analytics/store/actions';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import * as customStageActions from 'ee/analytics/cycle_analytics/store/modules/custom_stages/actions';
import * as customStageTypes from 'ee/analytics/cycle_analytics/store/modules/custom_stages/mutation_types';
import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import {
......@@ -14,7 +16,6 @@ import {
customizableStagesAndEvents,
endpoints,
} from '../mock_data';
import { shouldFlashAMessage } from '../helpers';
const stageData = { events: [] };
const error = new Error(`Request failed with status code ${httpStatusCodes.NOT_FOUND}`);
......@@ -25,10 +26,17 @@ const selectedStageSlug = selectedStage.slug;
const stageEndpoint = ({ stageId }) =>
`/groups/${selectedGroup.fullPath}/-/analytics/value_stream_analytics/stages/${stageId}`;
jest.mock('~/flash');
describe('Cycle analytics actions', () => {
let state;
let mock;
const shouldFlashAMessage = (msg, type = null) => {
const args = type ? [msg, type] : [msg];
expect(createFlash).toHaveBeenCalledWith(...args);
};
beforeEach(() => {
state = {
startDate,
......@@ -71,14 +79,13 @@ describe('Cycle analytics actions', () => {
describe('setDateRange', () => {
const payload = { startDate, endDate };
it('dispatches the fetchCycleAnalyticsData action', done => {
testAction(
it('dispatches the fetchCycleAnalyticsData action', () => {
return testAction(
actions.setDateRange,
payload,
state,
[{ type: types.SET_DATE_RANGE, payload: { startDate, endDate } }],
[{ type: 'fetchCycleAnalyticsData' }],
done,
);
});
});
......@@ -90,8 +97,8 @@ describe('Cycle analytics actions', () => {
mock.onGet(endpoints.stageData).reply(200, { events: [] });
});
it('dispatches receiveStageDataSuccess with received data on success', done => {
testAction(
it('dispatches receiveStageDataSuccess with received data on success', () => {
return testAction(
actions.fetchStageData,
selectedStageSlug,
state,
......@@ -103,7 +110,6 @@ describe('Cycle analytics actions', () => {
payload: { events: [] },
},
],
done,
);
});
......@@ -113,8 +119,8 @@ describe('Cycle analytics actions', () => {
mock.onGet(endpoints.stageData).replyOnce(httpStatusCodes.NOT_FOUND, { error });
});
it('dispatches receiveStageDataError on error', done => {
testAction(
it('dispatches receiveStageDataError on error', () => {
return testAction(
actions.fetchStageData,
selectedStage,
state,
......@@ -128,29 +134,25 @@ describe('Cycle analytics actions', () => {
payload: error,
},
],
done,
);
});
});
describe('receiveStageDataSuccess', () => {
it(`commits the ${types.RECEIVE_STAGE_DATA_SUCCESS} mutation`, done => {
testAction(
it(`commits the ${types.RECEIVE_STAGE_DATA_SUCCESS} mutation`, () => {
return testAction(
actions.receiveStageDataSuccess,
{ ...stageData },
state,
[{ type: types.RECEIVE_STAGE_DATA_SUCCESS, payload: { events: [] } }],
[],
done,
);
});
});
});
describe('receiveStageDataError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
beforeEach(() => {});
it(`commits the ${types.RECEIVE_STAGE_DATA_ERROR} mutation`, () => {
return testAction(
actions.receiveStageDataError,
......@@ -166,10 +168,7 @@ describe('Cycle analytics actions', () => {
});
it('will flash an error message', () => {
actions.receiveStageDataError({
commit: () => {},
});
actions.receiveStageDataError({ commit: () => {} });
shouldFlashAMessage('There was an error fetching data for the selected stage');
});
});
......@@ -197,11 +196,10 @@ describe('Cycle analytics actions', () => {
}
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
state = { ...state, selectedGroup, startDate, endDate };
});
it(`dispatches actions for required value stream analytics analytics data`, done => {
it(`dispatches actions for required value stream analytics analytics data`, () => {
testAction(
actions.fetchCycleAnalyticsData,
state,
......@@ -213,11 +211,10 @@ describe('Cycle analytics actions', () => {
{ type: 'fetchStageMedianValues' },
{ type: 'receiveCycleAnalyticsDataSuccess' },
],
done,
);
});
it(`displays an error if fetchStageMedianValues fails`, done => {
it(`displays an error if fetchStageMedianValues fails`, () => {
const { mockDispatchContext } = mockFetchCycleAnalyticsAction({
fetchStageMedianValues: actions.fetchStageMedianValues({
dispatch: jest
......@@ -230,7 +227,7 @@ describe('Cycle analytics actions', () => {
}),
});
actions
return actions
.fetchCycleAnalyticsData({
dispatch: mockDispatchContext,
state: {},
......@@ -238,25 +235,23 @@ describe('Cycle analytics actions', () => {
})
.then(() => {
shouldFlashAMessage('There was an error fetching median data for stages');
done();
})
.catch(done.fail);
});
});
it(`displays an error if fetchGroupStagesAndEvents fails`, done => {
it(`displays an error if fetchGroupStagesAndEvents fails`, () => {
const { mockDispatchContext } = mockFetchCycleAnalyticsAction({
fetchGroupStagesAndEvents: actions.fetchGroupStagesAndEvents({
dispatch: jest
.fn()
.mockResolvedValueOnce()
.mockImplementation(actions.receiveGroupStagesAndEventsError({ commit: () => {} })),
.mockImplementation(actions.receiveGroupStagesError({ commit: () => {} })),
commit: () => {},
state: { ...state },
getters,
}),
});
actions
return actions
.fetchCycleAnalyticsData({
dispatch: mockDispatchContext,
state: {},
......@@ -264,62 +259,16 @@ describe('Cycle analytics actions', () => {
})
.then(() => {
shouldFlashAMessage('There was an error fetching value stream analytics stages.');
done();
})
.catch(done.fail);
});
describe('with an existing error', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('removes an existing flash error if present', done => {
const { mockDispatchContext } = mockFetchCycleAnalyticsAction();
createFlash(flashErrorMessage);
const flashAlert = document.querySelector('.flash-alert');
expect(flashAlert).toBeVisible();
actions
.fetchCycleAnalyticsData({
dispatch: mockDispatchContext,
state: {},
commit: () => {},
})
.then(() => {
expect(flashAlert.style.opacity).toBe('0');
done();
})
.catch(done.fail);
});
});
it('will flash an error when there are no stages', () => {
[[], null].forEach(emptyStages => {
actions.receiveGroupStagesAndEventsSuccess(
{
commit: () => {},
state: { stages: emptyStages },
getters,
},
{},
);
shouldFlashAMessage(flashErrorMessage);
});
});
});
});
describe('receiveCycleAnalyticsDataError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
beforeEach(() => {});
it(`commits the ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} mutation on a 403 response`, done => {
it(`commits the ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} mutation on a 403 response`, () => {
const response = { status: 403 };
testAction(
return testAction(
actions.receiveCycleAnalyticsDataError,
{ response },
state,
......@@ -330,13 +279,12 @@ describe('Cycle analytics actions', () => {
},
],
[],
done,
);
});
it(`commits the ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} mutation on a non 403 error response`, done => {
it(`commits the ${types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR} mutation on a non 403 error response`, () => {
const response = { status: 500 };
testAction(
return testAction(
actions.receiveCycleAnalyticsDataError,
{ response },
state,
......@@ -347,7 +295,6 @@ describe('Cycle analytics actions', () => {
},
],
[],
done,
);
});
......@@ -364,64 +311,65 @@ describe('Cycle analytics actions', () => {
});
});
describe('receiveGroupStagesAndEventsSuccess', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
describe('receiveGroupStagesSuccess', () => {
beforeEach(() => {});
it(`commits the ${types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS} mutation`, done => {
testAction(
actions.receiveGroupStagesAndEventsSuccess,
{ ...customizableStagesAndEvents },
it(`commits the ${types.RECEIVE_GROUP_STAGES_SUCCESS} mutation and dispatches 'setDefaultSelectedStage'`, () => {
return testAction(
actions.receiveGroupStagesSuccess,
{ ...customizableStagesAndEvents.stages },
state,
[
{
type: types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS,
payload: { ...customizableStagesAndEvents },
type: types.RECEIVE_GROUP_STAGES_SUCCESS,
payload: { ...customizableStagesAndEvents.stages },
},
],
[],
done,
[{ type: 'setDefaultSelectedStage' }],
);
});
});
it("dispatches the 'fetchStageData' action", done => {
const stateWithStages = {
...state,
stages,
};
testAction(
actions.receiveGroupStagesAndEventsSuccess,
{ ...customizableStagesAndEvents },
stateWithStages,
[
{
type: types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS,
payload: { ...customizableStagesAndEvents },
},
],
describe('setDefaultSelectedStage', () => {
it("dispatches the 'fetchStageData' action", () => {
return testAction(
actions.setDefaultSelectedStage,
null,
{
activeStages: stages,
},
[],
[
{ type: 'setSelectedStage', payload: selectedStage },
{ type: 'fetchStageData', payload: selectedStageSlug },
],
done,
);
});
it('will flash an error when there are no stages', () => {
[[], null].forEach(emptyStages => {
actions.receiveGroupStagesAndEventsSuccess(
{
commit: () => {},
state: { stages: emptyStages },
},
{},
);
});
it.each`
data
${[]}
${null}
`('with $data will flash an error', ({ data }) => {
actions.setDefaultSelectedStage({ getters: { activeStages: data }, dispatch: () => {} }, {});
shouldFlashAMessage(flashErrorMessage);
});
it('will select the first active stage', () => {
stages[0].hidden = true;
return testAction(
actions.setDefaultSelectedStage,
null,
{
activeStages: getters.activeStages({ stages }),
},
[],
[
{ type: 'setSelectedStage', payload: stages[1] },
{ type: 'fetchStageData', payload: stages[1].slug },
],
);
});
});
describe('updateStage', () => {
......@@ -433,8 +381,8 @@ describe('Cycle analytics actions', () => {
state = { selectedGroup };
});
it('dispatches receiveUpdateStageSuccess with put request response data', done => {
testAction(
it('dispatches receiveUpdateStageSuccess and customStages/setSavingCustomStage', () => {
return testAction(
actions.updateStage,
{
id: stageId,
......@@ -444,35 +392,35 @@ describe('Cycle analytics actions', () => {
[],
[
{ type: 'requestUpdateStage' },
{ type: 'customStages/setSavingCustomStage' },
{
type: 'receiveUpdateStageSuccess',
payload,
},
],
done,
);
});
describe('with a failed request', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock = new MockAdapter(axios);
mock.onPut(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.NOT_FOUND);
});
it('dispatches receiveUpdateStageError', done => {
it('dispatches receiveUpdateStageError', () => {
const data = {
id: stageId,
name: 'issue',
...payload,
};
testAction(
return testAction(
actions.updateStage,
data,
state,
[],
[
{ type: 'requestUpdateStage' },
{ type: 'customStages/setSavingCustomStage' },
{
type: 'receiveUpdateStageError',
payload: {
......@@ -481,50 +429,49 @@ describe('Cycle analytics actions', () => {
},
},
],
done,
);
});
it('flashes an error if the stage name already exists', done => {
actions.receiveUpdateStageError(
{
commit: () => {},
state,
},
{
status: httpStatusCodes.UNPROCESSABLE_ENTITY,
responseData: {
errors: { name: ['is reserved'] },
it('flashes an error if the stage name already exists', () => {
return actions
.receiveUpdateStageError(
{
commit: () => {},
dispatch: () => Promise.resolve(),
state,
},
data: {
name: stageId,
{
status: httpStatusCodes.UNPROCESSABLE_ENTITY,
responseData: {
errors: { name: ['is reserved'] },
},
data: {
name: stageId,
},
},
},
);
shouldFlashAMessage(`'${stageId}' stage already exists`);
done();
)
.then(() => {
shouldFlashAMessage(`'${stageId}' stage already exists`);
});
});
it('flashes an error message', done => {
actions.receiveUpdateStageError(
{
commit: () => {},
state,
},
{ status: httpStatusCodes.BAD_REQUEST },
);
shouldFlashAMessage('There was a problem saving your custom stage, please try again');
done();
it('flashes an error message', () => {
return actions
.receiveUpdateStageError(
{
dispatch: () => Promise.resolve(),
commit: () => {},
state,
},
{ status: httpStatusCodes.BAD_REQUEST },
)
.then(() => {
shouldFlashAMessage('There was a problem saving your custom stage, please try again');
});
});
});
describe('receiveUpdateStageSuccess', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
const response = {
title: 'NEW - COOL',
};
......@@ -535,11 +482,14 @@ describe('Cycle analytics actions', () => {
response,
state,
[{ type: types.RECEIVE_UPDATE_STAGE_SUCCESS }],
[{ type: 'fetchGroupStagesAndEvents' }, { type: 'setSelectedStage', payload: response }],
[
{ type: 'fetchGroupStagesAndEvents' },
{ type: 'customStages/showEditForm', payload: response },
],
));
it('will flash a success message', () =>
actions
it('will flash a success message', () => {
return actions
.receiveUpdateStageSuccess(
{
dispatch: () => {},
......@@ -548,8 +498,9 @@ describe('Cycle analytics actions', () => {
response,
)
.then(() => {
shouldFlashAMessage('Stage data updated');
}));
shouldFlashAMessage('Stage data updated', 'notice');
});
});
describe('with an error', () => {
it('will flash an error message', () =>
......@@ -572,13 +523,12 @@ describe('Cycle analytics actions', () => {
const stageId = 'cool-stage';
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock.onDelete(stageEndpoint({ stageId })).replyOnce(200);
state = { selectedGroup };
});
it('dispatches receiveRemoveStageSuccess with put request response data', done => {
testAction(
it('dispatches receiveRemoveStageSuccess with put request response data', () => {
return testAction(
actions.removeStage,
stageId,
state,
......@@ -589,7 +539,6 @@ describe('Cycle analytics actions', () => {
type: 'receiveRemoveStageSuccess',
},
],
done,
);
});
......@@ -599,8 +548,8 @@ describe('Cycle analytics actions', () => {
mock.onDelete(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.NOT_FOUND);
});
it('dispatches receiveRemoveStageError', done => {
testAction(
it('dispatches receiveRemoveStageError', () => {
return testAction(
actions.removeStage,
stageId,
state,
......@@ -612,21 +561,12 @@ describe('Cycle analytics actions', () => {
payload: error,
},
],
done,
);
});
it('flashes an error message', done => {
actions.receiveRemoveStageError(
{
commit: () => {},
state,
},
{},
);
it('flashes an error message', () => {
actions.receiveRemoveStageError({ commit: () => {}, state }, {});
shouldFlashAMessage('There was an error removing your custom stage, please try again');
done();
});
});
});
......@@ -635,34 +575,31 @@ describe('Cycle analytics actions', () => {
const stageId = 'cool-stage';
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
mock.onDelete(stageEndpoint({ stageId })).replyOnce(200);
state = { selectedGroup };
});
it('dispatches fetchCycleAnalyticsData', done => {
testAction(
it('dispatches fetchCycleAnalyticsData', () => {
return testAction(
actions.receiveRemoveStageSuccess,
stageId,
state,
[{ type: 'RECEIVE_REMOVE_STAGE_RESPONSE' }],
[{ type: 'fetchCycleAnalyticsData' }],
done,
);
});
it('flashes a success message', done => {
actions.receiveRemoveStageSuccess(
{
dispatch: () => {},
commit: () => {},
state,
},
{},
);
shouldFlashAMessage('Stage removed');
done();
it('flashes a success message', () => {
return actions
.receiveRemoveStageSuccess(
{
dispatch: () => Promise.resolve(),
commit: () => {},
state,
},
{},
)
.then(() => shouldFlashAMessage('Stage removed', 'notice'));
});
});
......@@ -675,8 +612,8 @@ describe('Cycle analytics actions', () => {
mockDispatch = jest.fn();
});
it('dispatches receiveStageMedianValuesSuccess with received data on success', done => {
actions
it('dispatches receiveStageMedianValuesSuccess with received data on success', () => {
return actions
.fetchStageMedianValues({
state,
getters,
......@@ -688,9 +625,7 @@ describe('Cycle analytics actions', () => {
expect(mockDispatch).toHaveBeenCalledWith('receiveStageMedianValuesSuccess', [
{ events: [], id: selectedStageSlug },
]);
done();
})
.catch(done.fail);
});
});
describe('with a failing request', () => {
......@@ -698,8 +633,8 @@ describe('Cycle analytics actions', () => {
mock.onGet(endpoints.stageMedian).reply(httpStatusCodes.NOT_FOUND, { error });
});
it('will dispatch receiveStageMedianValuesError', done => {
actions
it('will dispatch receiveStageMedianValuesError', () => {
return actions
.fetchStageMedianValues({
state,
getters,
......@@ -709,19 +644,15 @@ describe('Cycle analytics actions', () => {
.then(() => {
expect(mockDispatch).toHaveBeenCalledWith('requestStageMedianValues');
expect(mockDispatch).toHaveBeenCalledWith('receiveStageMedianValuesError', error);
done();
})
.catch(done.fail);
});
});
});
});
describe('receiveStageMedianValuesError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
beforeEach(() => {});
it(`commits the ${types.RECEIVE_STAGE_MEDIANS_ERROR} mutation`, done => {
it(`commits the ${types.RECEIVE_STAGE_MEDIANS_ERROR} mutation`, () => {
testAction(
actions.receiveStageMedianValuesError,
null,
......@@ -732,33 +663,28 @@ describe('Cycle analytics actions', () => {
},
],
[],
done,
);
});
it('will flash an error message', () => {
actions.receiveStageMedianValuesError({
commit: () => {},
});
actions.receiveStageMedianValuesError({ commit: () => {} });
shouldFlashAMessage('There was an error fetching median data for stages');
});
});
describe('receiveStageMedianValuesSuccess', () => {
it(`commits the ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} mutation`, done => {
testAction(
it(`commits the ${types.RECEIVE_STAGE_MEDIANS_SUCCESS} mutation`, () => {
return testAction(
actions.receiveStageMedianValuesSuccess,
{ ...stageData },
state,
[{ type: types.RECEIVE_STAGE_MEDIANS_SUCCESS, payload: { events: [] } }],
[],
done,
);
});
});
describe('createCustomStage', () => {
describe('createStage', () => {
describe('with valid data', () => {
const customStageData = {
startEventIdentifier: 'start_event',
......@@ -771,16 +697,17 @@ describe('Cycle analytics actions', () => {
mock.onPost(endpoints.baseStagesEndpointstageData).reply(201, customStageData);
});
it(`dispatches the 'receiveCreateCustomStageSuccess' action`, () =>
it(`dispatches the 'receiveCreateStageSuccess' action`, () =>
testAction(
actions.createCustomStage,
customStageActions.createStage,
customStageData,
state,
[],
[
{ type: 'requestCreateCustomStage' },
{ type: 'clearFormErrors' },
{ type: 'setSavingCustomStage' },
{
type: 'receiveCreateCustomStageSuccess',
type: 'receiveCreateStageSuccess',
payload: { data: customStageData, status: 201 },
},
],
......@@ -808,16 +735,17 @@ describe('Cycle analytics actions', () => {
});
});
it(`dispatches the 'receiveCreateCustomStageError' action`, () =>
it(`dispatches the 'receiveCreateStageError' action`, () =>
testAction(
actions.createCustomStage,
customStageActions.createStage,
customStageData,
state,
[],
[
{ type: 'requestCreateCustomStage' },
{ type: 'clearFormErrors' },
{ type: 'setSavingCustomStage' },
{
type: 'receiveCreateCustomStageError',
type: 'receiveCreateStageError',
payload: {
data: customStageData,
errors,
......@@ -830,47 +758,58 @@ describe('Cycle analytics actions', () => {
});
});
describe('receiveCreateCustomStageError', () => {
describe('receiveCreateStageError', () => {
const response = {
data: { name: 'uh oh' },
};
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('will commit the RECEIVE_CREATE_CUSTOM_STAGE_ERROR mutation', () =>
testAction(actions.receiveCreateCustomStageError, response, state, [
{ type: types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR, payload: { errors: {} } },
]));
beforeEach(() => {});
it('will flash an error message', done => {
actions.receiveCreateCustomStageError(
{
commit: () => {},
},
it('will commit the RECEIVE_CREATE_STAGE_ERROR mutation', () =>
testAction(
customStageActions.receiveCreateStageError,
response,
);
shouldFlashAMessage('There was a problem saving your custom stage, please try again');
done();
});
describe('with a stage name error', () => {
it('will flash an error message', done => {
actions.receiveCreateCustomStageError(
state,
[{ type: customStageTypes.RECEIVE_CREATE_STAGE_ERROR }],
[
{
commit: () => {},
type: 'setStageFormErrors',
payload: {},
},
],
));
it('will flash an error message', () => {
return customStageActions
.receiveCreateStageError(
{
...response,
status: httpStatusCodes.UNPROCESSABLE_ENTITY,
errors: { name: ['is reserved'] },
dispatch: () => Promise.resolve(),
commit: () => {},
},
);
response,
)
.then(() => {
shouldFlashAMessage('There was a problem saving your custom stage, please try again');
});
});
shouldFlashAMessage("'uh oh' stage already exists");
done();
describe('with a stage name error', () => {
it('will flash an error message', () => {
return customStageActions
.receiveCreateStageError(
{
dispatch: () => Promise.resolve(),
commit: () => {},
},
{
...response,
status: httpStatusCodes.UNPROCESSABLE_ENTITY,
errors: { name: ['is reserved'] },
},
)
.then(() => {
shouldFlashAMessage("'uh oh' stage already exists");
});
});
});
});
......@@ -934,7 +873,7 @@ describe('Cycle analytics actions', () => {
));
});
describe('receiveCreateCustomStageSuccess', () => {
describe('receiveCreateStageSuccess', () => {
const response = {
data: {
title: 'COOL',
......@@ -943,21 +882,17 @@ describe('Cycle analytics actions', () => {
it('will dispatch fetchGroupStagesAndEvents', () =>
testAction(
actions.receiveCreateCustomStageSuccess,
customStageActions.receiveCreateStageSuccess,
response,
state,
[{ type: types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS }],
[{ type: 'fetchGroupStagesAndEvents' }],
[{ type: customStageTypes.RECEIVE_CREATE_STAGE_SUCCESS }],
[{ type: 'fetchGroupStagesAndEvents', payload: null }, { type: 'clearSavingCustomStage' }],
));
describe('with an error', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('will flash an error message', () =>
actions
.receiveCreateCustomStageSuccess(
customStageActions
.receiveCreateStageSuccess(
{
dispatch: () => Promise.reject(),
commit: () => {},
......@@ -983,14 +918,13 @@ describe('Cycle analytics actions', () => {
mock.onPut(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.OK);
});
it(`dispatches the ${types.REQUEST_REORDER_STAGE} and ${types.RECEIVE_REORDER_STAGE_SUCCESS} actions`, done => {
testAction(
it(`dispatches the ${types.REQUEST_REORDER_STAGE} and ${types.RECEIVE_REORDER_STAGE_SUCCESS} actions`, () => {
return testAction(
actions.reorderStage,
payload,
state,
[],
[{ type: 'requestReorderStage' }, { type: 'receiveReorderStageSuccess' }],
done,
);
});
});
......@@ -1000,8 +934,8 @@ describe('Cycle analytics actions', () => {
mock.onPut(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.NOT_FOUND);
});
it(`dispatches the ${types.REQUEST_REORDER_STAGE} and ${types.RECEIVE_REORDER_STAGE_ERROR} actions `, done => {
testAction(
it(`dispatches the ${types.REQUEST_REORDER_STAGE} and ${types.RECEIVE_REORDER_STAGE_ERROR} actions `, () => {
return testAction(
actions.reorderStage,
payload,
state,
......@@ -1010,18 +944,16 @@ describe('Cycle analytics actions', () => {
{ type: 'requestReorderStage' },
{ type: 'receiveReorderStageError', payload: { status: httpStatusCodes.NOT_FOUND } },
],
done,
);
});
});
});
describe('receiveReorderStageError', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
beforeEach(() => {});
it(`commits the ${types.RECEIVE_REORDER_STAGE_ERROR} mutation and flashes an error`, () => {
testAction(
return testAction(
actions.receiveReorderStageError,
null,
state,
......@@ -1031,23 +963,22 @@ describe('Cycle analytics actions', () => {
},
],
[],
);
shouldFlashAMessage(
'There was an error updating the stage order. Please try reloading the page.',
);
).then(() => {
shouldFlashAMessage(
'There was an error updating the stage order. Please try reloading the page.',
);
});
});
});
describe('receiveReorderStageSuccess', () => {
it(`commits the ${types.RECEIVE_REORDER_STAGE_SUCCESS} mutation`, done => {
testAction(
it(`commits the ${types.RECEIVE_REORDER_STAGE_SUCCESS} mutation`, () => {
return testAction(
actions.receiveReorderStageSuccess,
null,
state,
[{ type: types.RECEIVE_REORDER_STAGE_SUCCESS }],
[],
done,
);
});
});
......
import mutations from 'ee/analytics/cycle_analytics/store/mutations';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import customStageMutations from 'ee/analytics/cycle_analytics/store/modules/custom_stages/mutations';
import * as customStageTypes from 'ee/analytics/cycle_analytics/store/modules/custom_stages/mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
rawIssueEvents,
issueEvents as transformedEvents,
issueStage,
planStage,
codeStage,
......@@ -15,6 +15,7 @@ import {
endDate,
customizableStagesAndEvents,
selectedProjects,
rawCustomStage,
} from '../mock_data';
let state = null;
......@@ -29,43 +30,21 @@ describe('Cycle analytics mutations', () => {
});
it.each`
mutation | stateKey | value
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isEditingCustomStage'} | ${false}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'customStageFormErrors'} | ${null}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'customStageFormInitialData'} | ${null}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${true}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isEditingCustomStage'} | ${false}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'customStageFormErrors'} | ${null}
${types.SHOW_EDIT_CUSTOM_STAGE_FORM} | ${'isEditingCustomStage'} | ${true}
${types.SHOW_EDIT_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false}
${types.SHOW_EDIT_CUSTOM_STAGE_FORM} | ${'customStageFormErrors'} | ${null}
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'stages'} | ${[]}
${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'stages'} | ${[]}
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'customStageFormEvents'} | ${[]}
${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'customStageFormEvents'} | ${[]}
${types.REQUEST_CREATE_CUSTOM_STAGE} | ${'isSavingCustomStage'} | ${true}
${types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS} | ${'isSavingCustomStage'} | ${false}
${types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR} | ${'isSavingCustomStage'} | ${false}
${types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR} | ${'customStageFormErrors'} | ${{}}
${types.REQUEST_UPDATE_STAGE} | ${'isLoading'} | ${true}
${types.REQUEST_UPDATE_STAGE} | ${'isSavingCustomStage'} | ${true}
${types.REQUEST_UPDATE_STAGE} | ${'customStageFormErrors'} | ${null}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isLoading'} | ${false}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isSavingCustomStage'} | ${false}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isEditingCustomStage'} | ${false}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'customStageFormErrors'} | ${null}
${types.RECEIVE_UPDATE_STAGE_ERROR} | ${'isLoading'} | ${false}
${types.RECEIVE_UPDATE_STAGE_ERROR} | ${'isSavingCustomStage'} | ${false}
${types.REQUEST_REMOVE_STAGE} | ${'isLoading'} | ${true}
${types.RECEIVE_REMOVE_STAGE_RESPONSE} | ${'isLoading'} | ${false}
${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
${types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS} | ${'isLoading'} | ${false}
mutation | stateKey | value
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
${types.REQUEST_CYCLE_ANALYTICS_DATA} | ${'isLoading'} | ${true}
${types.RECEIVE_GROUP_STAGES_ERROR} | ${'stages'} | ${[]}
${types.REQUEST_GROUP_STAGES} | ${'stages'} | ${[]}
${types.REQUEST_UPDATE_STAGE} | ${'isLoading'} | ${true}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isLoading'} | ${false}
${types.RECEIVE_UPDATE_STAGE_ERROR} | ${'isLoading'} | ${false}
${types.REQUEST_REMOVE_STAGE} | ${'isLoading'} | ${true}
${types.RECEIVE_REMOVE_STAGE_RESPONSE} | ${'isLoading'} | ${false}
${types.REQUEST_STAGE_MEDIANS} | ${'medians'} | ${{}}
${types.RECEIVE_STAGE_MEDIANS_ERROR} | ${'medians'} | ${{}}
${types.INITIALIZE_CYCLE_ANALYTICS_SUCCESS} | ${'isLoading'} | ${false}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
mutations[mutation](state);
......@@ -91,41 +70,62 @@ describe('Cycle analytics mutations', () => {
},
);
describe(`${types.RECEIVE_STAGE_DATA_SUCCESS}`, () => {
it('will set the currentStageEvents state item with the camelCased events', () => {
mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state, rawIssueEvents);
expect(state.currentStageEvents).toEqual(transformedEvents);
describe('Custom stage mutations', () => {
beforeEach(() => {
state = {};
});
it('will set isLoadingStage=false', () => {
mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state);
expect(state.isLoadingStage).toEqual(false);
afterEach(() => {
state = null;
});
it('will set isEmptyStage=false if currentStageEvents.length > 0', () => {
mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state, rawIssueEvents);
expect(state.isEmptyStage).toEqual(false);
it.each`
mutation | stateKey | value
${customStageTypes.HIDE_FORM} | ${'isCreatingCustomStage'} | ${false}
${customStageTypes.HIDE_FORM} | ${'isEditingCustomStage'} | ${false}
${customStageTypes.HIDE_FORM} | ${'formErrors'} | ${null}
${customStageTypes.HIDE_FORM} | ${'formInitialData'} | ${null}
${customStageTypes.SHOW_CREATE_FORM} | ${'isCreatingCustomStage'} | ${true}
${customStageTypes.SHOW_CREATE_FORM} | ${'isEditingCustomStage'} | ${false}
${customStageTypes.SHOW_CREATE_FORM} | ${'formErrors'} | ${null}
${customStageTypes.SHOW_EDIT_FORM} | ${'isEditingCustomStage'} | ${true}
${customStageTypes.SHOW_EDIT_FORM} | ${'isCreatingCustomStage'} | ${false}
${customStageTypes.SHOW_EDIT_FORM} | ${'formErrors'} | ${null}
${customStageTypes.RECEIVE_CREATE_STAGE_ERROR} | ${'isSavingCustomStage'} | ${false}
${customStageTypes.SET_SAVING_CUSTOM_STAGE} | ${'isSavingCustomStage'} | ${true}
${customStageTypes.CLEAR_SAVING_CUSTOM_STAGE} | ${'isSavingCustomStage'} | ${false}
`('$mutation will set $stateKey=$value', ({ mutation, stateKey, value }) => {
customStageMutations[mutation](state);
expect(state[stateKey]).toEqual(value);
});
it('will set isEmptyStage=true if currentStageEvents.length <= 0', () => {
mutations[types.RECEIVE_STAGE_DATA_SUCCESS](state);
describe(`${customStageTypes.SET_STAGE_FORM_ERRORS}`, () => {
const mockFormError = { start_identifier: ['Cant be blank'] };
it('will set formErrors', () => {
state = {};
customStageMutations[customStageTypes.SET_STAGE_FORM_ERRORS](state, mockFormError);
expect(state.isEmptyStage).toEqual(true);
expect(state.formErrors).toEqual(convertObjectPropsToCamelCase(mockFormError));
});
});
});
describe(`types.RECEIVE_UPDATE_STAGE_ERROR`, () => {
const mockFormError = { errors: { start_identifier: ['Cant be blank'] } };
it('will set customStageFormErrors', () => {
state = {};
mutations[types.RECEIVE_UPDATE_STAGE_ERROR](state, mockFormError);
describe(`${customStageTypes.SET_FORM_INITIAL_DATA}`, () => {
const mockStage = {
id: 18,
name: 'Coolest beans stage',
startEventIdentifier: 'issue_first_mentioned_in_commit',
startEventLabelId: null,
endEventIdentifier: 'issue_first_added_to_board',
endEventLabelId: null,
};
expect(state.customStageFormErrors).toEqual(
convertObjectPropsToCamelCase(mockFormError.errors),
);
it('will set formInitialData', () => {
state = {};
customStageMutations[customStageTypes.SET_FORM_INITIAL_DATA](state, rawCustomStage);
expect(state.formInitialData).toEqual(mockStage);
});
});
});
......@@ -141,13 +141,10 @@ describe('Cycle analytics mutations', () => {
});
});
describe(`${types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS}`, () => {
describe(`${types.RECEIVE_GROUP_STAGES_SUCCESS}`, () => {
describe('with data', () => {
beforeEach(() => {
mutations[types.RECEIVE_GROUP_STAGES_AND_EVENTS_SUCCESS](
state,
customizableStagesAndEvents,
);
mutations[types.RECEIVE_GROUP_STAGES_SUCCESS](state, customizableStagesAndEvents.stages);
});
it('will convert the stats object to stages', () => {
......
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