Commit 737d8993 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch 'fix-handle-ca-custom-form-server-errors' into 'master'

Handle ca custom form server errors

Closes #36685

See merge request gitlab-org/gitlab!24218
parents 85bd310f 8b2aa804
...@@ -15,8 +15,8 @@ module Analytics ...@@ -15,8 +15,8 @@ module Analytics
validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom? validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
validates :start_event_identifier, presence: true validates :start_event_identifier, presence: true
validates :end_event_identifier, presence: true validates :end_event_identifier, presence: true
validates :start_event_label, presence: true, if: :start_event_label_based? validates :start_event_label_id, presence: true, if: :start_event_label_based?
validates :end_event_label, presence: true, if: :end_event_label_based? validates :end_event_label_id, presence: true, if: :end_event_label_based?
validate :validate_stage_event_pairs validate :validate_stage_event_pairs
validate :validate_labels validate :validate_labels
...@@ -109,8 +109,8 @@ module Analytics ...@@ -109,8 +109,8 @@ module Analytics
end end
def validate_labels def validate_labels
validate_label_within_group(:start_event_label, start_event_label_id) if start_event_label_id_changed? validate_label_within_group(:start_event_label_id, start_event_label_id) if start_event_label_id_changed?
validate_label_within_group(:end_event_label, end_event_label_id) if end_event_label_id_changed? validate_label_within_group(:end_event_label_id, end_event_label_id) if end_event_label_id_changed?
end end
def validate_label_within_group(association_name, label_id) def validate_label_within_group(association_name, label_id)
......
...@@ -68,6 +68,7 @@ export default { ...@@ -68,6 +68,7 @@ export default {
'endDate', 'endDate',
'tasksByType', 'tasksByType',
'medians', 'medians',
'customStageFormErrors',
]), ]),
...mapGetters([ ...mapGetters([
'hasNoAccessError', 'hasNoAccessError',
...@@ -135,6 +136,7 @@ export default { ...@@ -135,6 +136,7 @@ export default {
'setSelectedStage', 'setSelectedStage',
'hideCustomStageForm', 'hideCustomStageForm',
'showCustomStageForm', 'showCustomStageForm',
'showEditCustomStageForm',
'setDateRange', 'setDateRange',
'fetchTasksByTypeData', 'fetchTasksByTypeData',
'updateSelectedDurationChartStages', 'updateSelectedDurationChartStages',
...@@ -142,7 +144,7 @@ export default { ...@@ -142,7 +144,7 @@ export default {
'updateStage', 'updateStage',
'removeStage', 'removeStage',
'setFeatureFlags', 'setFeatureFlags',
'editCustomStage', 'clearCustomStageFormErrors',
'updateStage', 'updateStage',
'setTasksByTypeFilters', 'setTasksByTypeFilters',
]), ]),
...@@ -164,7 +166,7 @@ export default { ...@@ -164,7 +166,7 @@ export default {
this.showCustomStageForm(); this.showCustomStageForm();
}, },
onShowEditStageForm(initData = {}) { onShowEditStageForm(initData = {}) {
this.editCustomStage(initData); this.showEditCustomStageForm(initData);
}, },
initDateRange() { initDateRange() {
const endDate = new Date(Date.now()); const endDate = new Date(Date.now());
...@@ -278,10 +280,12 @@ export default { ...@@ -278,10 +280,12 @@ export default {
:is-editing-custom-stage="isEditingCustomStage" :is-editing-custom-stage="isEditingCustomStage"
:current-stage-events="currentStageEvents" :current-stage-events="currentStageEvents"
:custom-stage-form-events="customStageFormEvents" :custom-stage-form-events="customStageFormEvents"
:custom-stage-form-errors="customStageFormErrors"
:labels="labels" :labels="labels"
:no-data-svg-path="noDataSvgPath" :no-data-svg-path="noDataSvgPath"
:no-access-svg-path="noAccessSvgPath" :no-access-svg-path="noAccessSvgPath"
:can-edit-stages="hasCustomizableCycleAnalytics" :can-edit-stages="hasCustomizableCycleAnalytics"
@clearCustomStageFormErrors="clearCustomStageFormErrors"
@selectStage="onStageSelect" @selectStage="onStageSelect"
@editStage="onShowEditStageForm" @editStage="onShowEditStageForm"
@showAddStageForm="onShowAddStageForm" @showAddStageForm="onShowAddStageForm"
......
...@@ -14,7 +14,7 @@ import { ...@@ -14,7 +14,7 @@ import {
getLabelEventsIdentifiers, getLabelEventsIdentifiers,
} from '../utils'; } from '../utils';
const initFields = { const defaultFields = {
id: null, id: null,
name: null, name: null,
startEventIdentifier: null, startEventIdentifier: null,
...@@ -55,13 +55,27 @@ export default { ...@@ -55,13 +55,27 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
errors: {
type: Object,
required: false,
default: () => ({}),
},
}, },
data() { data() {
const defaultErrors = this?.initialFields?.endEventIdentifier
? {}
: { endEventIdentifier: [s__('CustomCycleAnalytics|Please select a start event first')] };
return { return {
labelEvents: getLabelEventsIdentifiers(this.events),
fields: { fields: {
...initFields, ...defaultFields,
...this.initialFields, ...this.initialFields,
}, },
fieldErrors: {
...defaultFields,
...this.errors,
...defaultErrors,
},
}; };
}, },
computed: { computed: {
...@@ -88,7 +102,7 @@ export default { ...@@ -88,7 +102,7 @@ export default {
return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier); return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier);
}, },
isComplete() { isComplete() {
if (!this.hasValidStartAndEndEventPair) { if (this.eventMismatchError) {
return false; return false;
} }
const { const {
...@@ -113,22 +127,16 @@ export default { ...@@ -113,22 +127,16 @@ export default {
); );
}, },
isDirty() { isDirty() {
return !isEqual(this.initialFields, this.fields) && !isEqual(initFields, this.fields); return !isEqual(this.initialFields, this.fields) && !isEqual(defaultFields, this.fields);
}, },
hasValidStartAndEndEventPair() { eventMismatchError() {
const { const {
fields: { startEventIdentifier, endEventIdentifier }, fields: { startEventIdentifier = null, endEventIdentifier = null },
} = this; } = this;
if (startEventIdentifier && endEventIdentifier) {
const endEvents = getAllowedEndEvents(this.events, startEventIdentifier); if (!startEventIdentifier || !endEventIdentifier) return true;
return endEvents.length && endEvents.includes(endEventIdentifier); const endEvents = getAllowedEndEvents(this.events, startEventIdentifier);
} return !endEvents.length || !endEvents.includes(endEventIdentifier);
return true;
},
endEventError() {
return !this.hasValidStartAndEndEventPair
? s__('CustomCycleAnalytics|Start event changed, please select a valid stop event')
: null;
}, },
saveStageText() { saveStageText() {
return this.isEditingCustomStage return this.isEditingCustomStage
...@@ -141,13 +149,22 @@ export default { ...@@ -141,13 +149,22 @@ export default {
: s__('CustomCycleAnalytics|New stage'); : s__('CustomCycleAnalytics|New stage');
}, },
}, },
mounted() { watch: {
this.labelEvents = getLabelEventsIdentifiers(this.events); initialFields(newFields) {
this.fields = {
...defaultFields,
...newFields,
};
this.fieldErrors = {
...defaultFields,
...this.errors,
};
},
}, },
methods: { methods: {
handleCancel() { handleCancel() {
this.fields = { this.fields = {
...initFields, ...defaultFields,
...this.initialFields, ...this.initialFields,
}; };
this.$emit('cancel'); this.$emit('cancel');
...@@ -167,6 +184,24 @@ export default { ...@@ -167,6 +184,24 @@ export default {
handleClearLabel(key) { handleClearLabel(key) {
this.fields[key] = null; this.fields[key] = null;
}, },
hasFieldErrors(key) {
return this.fieldErrors[key]?.length > 0;
},
fieldErrorMessage(key) {
return this.fieldErrors[key]?.join('\n');
},
onUpdateStartEventField() {
const initVal = this.initialFields?.endEventIdentifier
? this.initialFields.endEventIdentifier
: null;
this.$set(this.fields, 'endEventIdentifier', initVal);
this.$set(this.fieldErrors, 'endEventIdentifier', [
s__('CustomCycleAnalytics|Start event changed, please select a valid stop event'),
]);
},
onUpdateEndEventField() {
this.$set(this.fieldErrors, 'endEventIdentifier', null);
},
}, },
}; };
</script> </script>
...@@ -175,7 +210,13 @@ export default { ...@@ -175,7 +210,13 @@ export default {
<div class="mb-1"> <div class="mb-1">
<h4>{{ formTitle }}</h4> <h4>{{ formTitle }}</h4>
</div> </div>
<gl-form-group :label="s__('CustomCycleAnalytics|Name')">
<gl-form-group
ref="name"
:label="s__('CustomCycleAnalytics|Name')"
:state="!hasFieldErrors('name')"
:invalid-feedback="fieldErrorMessage('name')"
>
<gl-form-input <gl-form-input
v-model="fields.name" v-model="fields.name"
class="form-control" class="form-control"
...@@ -187,17 +228,28 @@ export default { ...@@ -187,17 +228,28 @@ export default {
</gl-form-group> </gl-form-group>
<div class="d-flex" :class="{ 'justify-content-between': startEventRequiresLabel }"> <div class="d-flex" :class="{ 'justify-content-between': startEventRequiresLabel }">
<div :class="[startEventRequiresLabel ? 'w-50 mr-1' : 'w-100']"> <div :class="[startEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<gl-form-group :label="s__('CustomCycleAnalytics|Start event')"> <gl-form-group
ref="startEventIdentifier"
:label="s__('CustomCycleAnalytics|Start event')"
:state="!hasFieldErrors('startEventIdentifier')"
:invalid-feedback="fieldErrorMessage('startEventIdentifier')"
>
<gl-form-select <gl-form-select
v-model="fields.startEventIdentifier" v-model="fields.startEventIdentifier"
name="custom-stage-start-event" name="custom-stage-start-event"
:required="true" :required="true"
:options="startEventOptions" :options="startEventOptions"
@change.native="onUpdateStartEventField"
/> />
</gl-form-group> </gl-form-group>
</div> </div>
<div v-if="startEventRequiresLabel" class="w-50 ml-1"> <div v-if="startEventRequiresLabel" class="w-50 ml-1">
<gl-form-group :label="s__('CustomCycleAnalytics|Start event label')"> <gl-form-group
ref="startEventLabelId"
:label="s__('CustomCycleAnalytics|Start event label')"
:state="!hasFieldErrors('startEventLabelId')"
:invalid-feedback="fieldErrorMessage('startEventLabelId')"
>
<labels-selector <labels-selector
:labels="labels" :labels="labels"
:selected-label-id="fields.startEventLabelId" :selected-label-id="fields.startEventLabelId"
...@@ -211,12 +263,11 @@ export default { ...@@ -211,12 +263,11 @@ export default {
<div class="d-flex" :class="{ 'justify-content-between': endEventRequiresLabel }"> <div class="d-flex" :class="{ 'justify-content-between': endEventRequiresLabel }">
<div :class="[endEventRequiresLabel ? 'w-50 mr-1' : 'w-100']"> <div :class="[endEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<gl-form-group <gl-form-group
ref="endEventIdentifier"
:label="s__('CustomCycleAnalytics|Stop event')" :label="s__('CustomCycleAnalytics|Stop event')"
:description=" :state="!hasFieldErrors('endEventIdentifier')"
!hasStartEvent ? s__('CustomCycleAnalytics|Please select a start event first') : '' :invalid-feedback="fieldErrorMessage('endEventIdentifier')"
" @change.native="onUpdateEndEventField"
:state="hasValidStartAndEndEventPair"
:invalid-feedback="endEventError"
> >
<gl-form-select <gl-form-select
v-model="fields.endEventIdentifier" v-model="fields.endEventIdentifier"
...@@ -228,7 +279,12 @@ export default { ...@@ -228,7 +279,12 @@ export default {
</gl-form-group> </gl-form-group>
</div> </div>
<div v-if="endEventRequiresLabel" class="w-50 ml-1"> <div v-if="endEventRequiresLabel" class="w-50 ml-1">
<gl-form-group :label="s__('CustomCycleAnalytics|Stop event label')"> <gl-form-group
ref="endEventLabelId"
:label="s__('CustomCycleAnalytics|Stop event label')"
:state="!hasFieldErrors('endEventLabelId')"
:invalid-feedback="fieldErrorMessage('endEventLabelId')"
>
<labels-selector <labels-selector
:labels="labels" :labels="labels"
:selected-label-id="fields.endEventLabelId" :selected-label-id="fields.endEventLabelId"
......
<script> <script>
import { mapState } from 'vuex';
import { GlTooltipDirective, GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; import { GlTooltipDirective, GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import StageNavItem from './stage_nav_item.vue'; import StageNavItem from './stage_nav_item.vue';
...@@ -63,6 +64,11 @@ export default { ...@@ -63,6 +64,11 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
customStageFormErrors: {
type: Object,
required: false,
default: () => {},
},
labels: { labels: {
type: Array, type: Array,
required: true, required: true,
...@@ -86,6 +92,7 @@ export default { ...@@ -86,6 +92,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['customStageFormInitialData']),
stageEventsHeight() { stageEventsHeight() {
return `${this.stageNavHeight}px`; return `${this.stageNavHeight}px`;
}, },
...@@ -127,27 +134,6 @@ export default { ...@@ -127,27 +134,6 @@ export default {
}, },
]; ];
}, },
customStageInitialData() {
if (this.isEditingCustomStage) {
const {
id = null,
name = null,
startEventIdentifier = null,
startEventLabel: { id: startEventLabelId = null } = {},
endEventIdentifier = null,
endEventLabel: { id: endEventLabelId = null } = {},
} = this.currentStage;
return {
id,
name,
startEventIdentifier,
startEventLabelId,
endEventIdentifier,
endEventLabelId,
};
}
return {};
},
}, },
mounted() { mounted() {
this.$set(this, 'stageNavHeight', this.$refs.stageNav.clientHeight); this.$set(this, 'stageNavHeight', this.$refs.stageNav.clientHeight);
...@@ -207,11 +193,13 @@ export default { ...@@ -207,11 +193,13 @@ export default {
:events="customStageFormEvents" :events="customStageFormEvents"
:labels="labels" :labels="labels"
:is-saving-custom-stage="isSavingCustomStage" :is-saving-custom-stage="isSavingCustomStage"
:initial-fields="customStageInitialData" :initial-fields="customStageFormInitialData"
:is-editing-custom-stage="isEditingCustomStage" :is-editing-custom-stage="isEditingCustomStage"
:errors="customStageFormErrors"
@submit="$emit('submit', $event)" @submit="$emit('submit', $event)"
@createStage="$emit($options.STAGE_ACTIONS.CREATE, $event)" @createStage="$emit($options.STAGE_ACTIONS.CREATE, $event)"
@updateStage="$emit($options.STAGE_ACTIONS.UPDATE, $event)" @updateStage="$emit($options.STAGE_ACTIONS.UPDATE, $event)"
@clearErrors="$emit('clearCustomStageFormErrors')"
/> />
<template v-else> <template v-else>
<stage-event-list <stage-event-list
......
...@@ -2,7 +2,7 @@ import dateFormat from 'dateformat'; ...@@ -2,7 +2,7 @@ import dateFormat from 'dateformat';
import Api from 'ee/api'; import Api from 'ee/api';
import { getDayDifference, getDateInPast } from '~/lib/utils/datetime_utility'; import { getDayDifference, getDateInPast } from '~/lib/utils/datetime_utility';
import createFlash, { hideFlash } from '~/flash'; import createFlash, { hideFlash } from '~/flash';
import { __ } from '~/locale'; import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { dateFormats } from '../../shared/constants'; import { dateFormats } from '../../shared/constants';
...@@ -21,6 +21,14 @@ const handleErrorOrRethrow = ({ action, error }) => { ...@@ -21,6 +21,14 @@ const handleErrorOrRethrow = ({ action, error }) => {
action(); action();
}; };
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;
};
export const setFeatureFlags = ({ commit }, featureFlags) => export const setFeatureFlags = ({ commit }, featureFlags) =>
commit(types.SET_FEATURE_FLAGS, featureFlags); commit(types.SET_FEATURE_FLAGS, featureFlags);
export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group); export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group);
...@@ -135,12 +143,36 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => { ...@@ -135,12 +143,36 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
.catch(error => dispatch('receiveCycleAnalyticsDataError', error)); .catch(error => dispatch('receiveCycleAnalyticsDataError', error));
}; };
export const hideCustomStageForm = ({ commit }) => commit(types.HIDE_CUSTOM_STAGE_FORM); export const hideCustomStageForm = ({ commit }) => {
export const showCustomStageForm = ({ commit }) => commit(types.SHOW_CUSTOM_STAGE_FORM); commit(types.HIDE_CUSTOM_STAGE_FORM);
removeError();
};
export const showCustomStageForm = ({ commit }) => {
commit(types.SHOW_CUSTOM_STAGE_FORM);
removeError();
};
export const editCustomStage = ({ commit, dispatch }, selectedStage = {}) => { export const showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {}) => {
commit(types.EDIT_CUSTOM_STAGE); 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); dispatch('setSelectedStage', selectedStage);
removeError();
}; };
export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA); export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA);
...@@ -236,27 +268,36 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => { ...@@ -236,27 +268,36 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
); );
}; };
export const clearCustomStageFormErrors = ({ commit }) => {
commit(types.CLEAR_CUSTOM_STAGE_FORM_ERRORS);
removeError();
};
export const requestCreateCustomStage = ({ commit }) => commit(types.REQUEST_CREATE_CUSTOM_STAGE); export const requestCreateCustomStage = ({ commit }) => commit(types.REQUEST_CREATE_CUSTOM_STAGE);
export const receiveCreateCustomStageSuccess = ({ commit, dispatch }, { data: { title } }) => { export const receiveCreateCustomStageSuccess = ({ commit, dispatch }, { data: { title } }) => {
commit(types.RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE); commit(types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS);
createFlash(__(`Your custom stage '${title}' was created`), 'notice'); createFlash(sprintf(__(`Your custom stage '%{title}' was created`), { title }), 'notice');
return dispatch('fetchGroupStagesAndEvents').then(() => dispatch('fetchSummaryData')); return Promise.resolve()
.then(() => dispatch('fetchGroupStagesAndEvents'))
.then(() => dispatch('fetchSummaryData'))
.catch(() => {
createFlash(__('There was a problem refreshing the data, please try again'));
});
}; };
export const receiveCreateCustomStageError = ({ commit }, { error, data }) => { export const receiveCreateCustomStageError = (
commit(types.RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE); { commit },
{ status = 400, errors = {}, data = {} } = {},
const { name } = data; ) => {
const { status } = error; commit(types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR, { errors });
// TODO: check for 403, 422 etc const { name = null } = data;
// Follow up issue to investigate https://gitlab.com/gitlab-org/gitlab/issues/36685 const flashMessage =
const message = name && isStageNameExistsError({ status, errors })
status !== httpStatus.UNPROCESSABLE_ENTITY ? sprintf(__(`'%{name}' stage already exists`), { name })
? __(`'${name}' stage already exists'`)
: __('There was a problem saving your custom stage, please try again'); : __('There was a problem saving your custom stage, please try again');
createFlash(message); createFlash(flashMessage);
}; };
export const createCustomStage = ({ dispatch, state }, data) => { export const createCustomStage = ({ dispatch, state }, data) => {
...@@ -266,8 +307,15 @@ export const createCustomStage = ({ dispatch, state }, data) => { ...@@ -266,8 +307,15 @@ export const createCustomStage = ({ dispatch, state }, data) => {
dispatch('requestCreateCustomStage'); dispatch('requestCreateCustomStage');
return Api.cycleAnalyticsCreateStage(fullPath, data) return Api.cycleAnalyticsCreateStage(fullPath, data)
.then(response => dispatch('receiveCreateCustomStageSuccess', response)) .then(response => {
.catch(error => dispatch('receiveCreateCustomStageError', { error, data })); 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 receiveTasksByTypeDataSuccess = ({ commit }, data) => { export const receiveTasksByTypeDataSuccess = ({ commit }, data) => {
...@@ -313,28 +361,28 @@ export const fetchTasksByTypeData = ({ dispatch, state, getters }) => { ...@@ -313,28 +361,28 @@ export const fetchTasksByTypeData = ({ dispatch, state, getters }) => {
export const requestUpdateStage = ({ commit }) => commit(types.REQUEST_UPDATE_STAGE); export const requestUpdateStage = ({ commit }) => commit(types.REQUEST_UPDATE_STAGE);
export const receiveUpdateStageSuccess = ({ commit, dispatch }, updatedData) => { export const receiveUpdateStageSuccess = ({ commit, dispatch }, updatedData) => {
commit(types.RECEIVE_UPDATE_STAGE_RESPONSE); commit(types.RECEIVE_UPDATE_STAGE_SUCCESS);
createFlash(__('Stage data updated'), 'notice'); createFlash(__('Stage data updated'), 'notice');
dispatch('fetchGroupStagesAndEvents'); return Promise.all([
dispatch('setSelectedStage', updatedData); dispatch('fetchGroupStagesAndEvents'),
dispatch('setSelectedStage', updatedData),
]).catch(() => {
createFlash(__('There was a problem refreshing the data, please try again'));
});
}; };
export const receiveUpdateStageError = ( export const receiveUpdateStageError = (
{ commit }, { commit },
{ error: { response: { status = 400, data: errorData } = {} } = {}, data }, { status, responseData: { errors = null } = {}, data = {} },
) => { ) => {
commit(types.RECEIVE_UPDATE_STAGE_RESPONSE); commit(types.RECEIVE_UPDATE_STAGE_ERROR, { errors, data });
const ERROR_NAME_RESERVED = 'is reserved';
let message = __('There was a problem saving your custom stage, please try again'); const { name = null } = data;
if (status && status === httpStatus.UNPROCESSABLE_ENTITY) { const message =
const { errors } = errorData; name && isStageNameExistsError({ status, errors })
const { name } = data; ? sprintf(__(`'%{name}' stage already exists`), { name })
if (errors.name && errors.name[0] === ERROR_NAME_RESERVED) { : __('There was a problem saving your custom stage, please try again');
message = __(`'${name}' stage already exists`);
}
}
createFlash(__(message)); createFlash(__(message));
}; };
...@@ -348,7 +396,9 @@ export const updateStage = ({ dispatch, state }, { id, ...rest }) => { ...@@ -348,7 +396,9 @@ export const updateStage = ({ dispatch, state }, { id, ...rest }) => {
return Api.cycleAnalyticsUpdateStage(id, fullPath, { ...rest }) return Api.cycleAnalyticsUpdateStage(id, fullPath, { ...rest })
.then(({ data }) => dispatch('receiveUpdateStageSuccess', data)) .then(({ data }) => dispatch('receiveUpdateStageSuccess', data))
.catch(error => dispatch('receiveUpdateStageError', { error, data: { id, ...rest } })); .catch(({ response: { status = 400, data: responseData } = {} }) =>
dispatch('receiveUpdateStageError', { status, responseData, data: { id, ...rest } }),
);
}; };
export const requestRemoveStage = ({ commit }) => commit(types.REQUEST_REMOVE_STAGE); export const requestRemoveStage = ({ commit }) => commit(types.REQUEST_REMOVE_STAGE);
......
...@@ -21,7 +21,8 @@ export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR'; ...@@ -21,7 +21,8 @@ export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR';
export const HIDE_CUSTOM_STAGE_FORM = 'HIDE_CUSTOM_STAGE_FORM'; export const HIDE_CUSTOM_STAGE_FORM = 'HIDE_CUSTOM_STAGE_FORM';
export const SHOW_CUSTOM_STAGE_FORM = 'SHOW_CUSTOM_STAGE_FORM'; export const SHOW_CUSTOM_STAGE_FORM = 'SHOW_CUSTOM_STAGE_FORM';
export const EDIT_CUSTOM_STAGE = 'EDIT_CUSTOM_STAGE'; 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_LABELS = 'REQUEST_GROUP_LABELS'; export const REQUEST_GROUP_LABELS = 'REQUEST_GROUP_LABELS';
export const RECEIVE_GROUP_LABELS_SUCCESS = 'RECEIVE_GROUP_LABELS_SUCCESS'; export const RECEIVE_GROUP_LABELS_SUCCESS = 'RECEIVE_GROUP_LABELS_SUCCESS';
...@@ -43,10 +44,12 @@ export const RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS = 'RECEIVE_TASKS_BY_TYPE_DATA_SU ...@@ -43,10 +44,12 @@ export const RECEIVE_TASKS_BY_TYPE_DATA_SUCCESS = 'RECEIVE_TASKS_BY_TYPE_DATA_SU
export const RECEIVE_TASKS_BY_TYPE_DATA_ERROR = 'RECEIVE_TASKS_BY_TYPE_DATA_ERROR'; export const RECEIVE_TASKS_BY_TYPE_DATA_ERROR = 'RECEIVE_TASKS_BY_TYPE_DATA_ERROR';
export const REQUEST_CREATE_CUSTOM_STAGE = 'REQUEST_CREATE_CUSTOM_STAGE'; export const REQUEST_CREATE_CUSTOM_STAGE = 'REQUEST_CREATE_CUSTOM_STAGE';
export const RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE = 'RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE'; 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_UPDATE_STAGE = 'REQUEST_UPDATE_STAGE'; export const REQUEST_UPDATE_STAGE = 'REQUEST_UPDATE_STAGE';
export const RECEIVE_UPDATE_STAGE_RESPONSE = 'RECEIVE_UPDATE_STAGE_RESPONSE'; export const RECEIVE_UPDATE_STAGE_SUCCESS = 'RECEIVE_UPDATE_STAGE_SUCCESS';
export const RECEIVE_UPDATE_STAGE_ERROR = 'RECEIVE_UPDATE_STAGE_ERROR';
export const REQUEST_REMOVE_STAGE = 'REQUEST_REMOVE_STAGE'; export const REQUEST_REMOVE_STAGE = 'REQUEST_REMOVE_STAGE';
export const RECEIVE_REMOVE_STAGE_RESPONSE = 'RECEIVE_REMOVE_STAGE_RESPONSE'; export const RECEIVE_REMOVE_STAGE_RESPONSE = 'RECEIVE_REMOVE_STAGE_RESPONSE';
......
...@@ -96,18 +96,24 @@ export default { ...@@ -96,18 +96,24 @@ export default {
}, },
[types.SHOW_CUSTOM_STAGE_FORM](state) { [types.SHOW_CUSTOM_STAGE_FORM](state) {
state.isCreatingCustomStage = true; state.isCreatingCustomStage = true;
state.customStageFormInitData = {}; state.isEditingCustomStage = false;
state.customStageFormInitialData = null;
state.customStageFormErrors = null;
}, },
[types.EDIT_CUSTOM_STAGE](state) { [types.SHOW_EDIT_CUSTOM_STAGE_FORM](state, initialData) {
state.isEditingCustomStage = true; state.isEditingCustomStage = true;
state.isCreatingCustomStage = false;
state.customStageFormInitialData = initialData;
state.customStageFormErrors = null;
}, },
[types.HIDE_CUSTOM_STAGE_FORM](state) { [types.HIDE_CUSTOM_STAGE_FORM](state) {
state.isEditingCustomStage = false; state.isEditingCustomStage = false;
state.isCreatingCustomStage = false; state.isCreatingCustomStage = false;
state.customStageFormInitData = {}; state.customStageFormInitialData = null;
state.customStageFormErrors = null;
}, },
[types.SHOW_CUSTOM_STAGE_FORM](state) { [types.CLEAR_CUSTOM_STAGE_FORM_ERRORS](state) {
state.isCreatingCustomStage = true; state.customStageFormErrors = null;
}, },
[types.RECEIVE_SUMMARY_DATA_ERROR](state) { [types.RECEIVE_SUMMARY_DATA_ERROR](state) {
state.summary = []; state.summary = [];
...@@ -152,16 +158,34 @@ export default { ...@@ -152,16 +158,34 @@ export default {
}, },
[types.REQUEST_CREATE_CUSTOM_STAGE](state) { [types.REQUEST_CREATE_CUSTOM_STAGE](state) {
state.isSavingCustomStage = true; 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_RESPONSE](state) { [types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS](state) {
state.isSavingCustomStage = false; state.isSavingCustomStage = false;
state.customStageFormErrors = null;
state.customStageFormInitialData = null;
}, },
[types.REQUEST_UPDATE_STAGE](state) { [types.REQUEST_UPDATE_STAGE](state) {
state.isLoading = true; 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_RESPONSE](state) { [types.RECEIVE_UPDATE_STAGE_ERROR](state, { errors = null, data } = {}) {
state.isLoading = false; state.isLoading = false;
state.isSavingCustomStage = false; state.isSavingCustomStage = false;
state.customStageFormErrors = convertObjectPropsToCamelCase(errors, { deep: true });
state.customStageFormInitialData = convertObjectPropsToCamelCase(data, { deep: true });
}, },
[types.REQUEST_REMOVE_STAGE](state) { [types.REQUEST_REMOVE_STAGE](state) {
state.isLoading = true; state.isLoading = true;
......
...@@ -31,6 +31,9 @@ export default () => ({ ...@@ -31,6 +31,9 @@ export default () => ({
medians: {}, medians: {},
customStageFormEvents: [], customStageFormEvents: [],
customStageFormErrors: null,
customStageFormInitialData: null,
tasksByType: { tasksByType: {
subject: TASKS_BY_TYPE_SUBJECT_ISSUE, subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
labelIds: [], labelIds: [],
......
...@@ -120,11 +120,11 @@ module Analytics ...@@ -120,11 +120,11 @@ module Analytics
end end
def update_params def update_params
params.permit(:name, :start_event_identifier, :end_event_identifier, :id, :move_after_id, :move_before_id, :hidden) params.permit(:name, :start_event_identifier, :end_event_identifier, :id, :move_after_id, :move_before_id, :hidden, :start_event_label_id, :end_event_label_id)
end end
def create_params def create_params
params.permit(:name, :start_event_identifier, :end_event_identifier) params.permit(:name, :start_event_identifier, :end_event_identifier, :start_event_label_id, :end_event_label_id)
end end
def delete_params def delete_params
......
...@@ -4,13 +4,15 @@ require 'spec_helper' ...@@ -4,13 +4,15 @@ require 'spec_helper'
describe 'Group Value Stream Analytics', :js do describe 'Group Value Stream Analytics', :js do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let!(:group) { create(:group, name: "CA-test-group") } let!(:group) { create(:group, name: "CA-test-group") }
let!(:group2) { create(:group, name: "CA-bad-test-group") }
let!(:project) { create(:project, :repository, namespace: group, group: group, name: "Cool fun project") } let!(:project) { create(:project, :repository, namespace: group, group: group, name: "Cool fun project") }
let!(:label) { create(:group_label, group: group) }
let!(:label2) { create(:group_label, group: group) }
let!(:label3) { create(:group_label, group: group2) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") } let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) } let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
let(:label) { create(:group_label, group: group) }
let(:label2) { create(:group_label, group: group) }
stage_nav_selector = '.stage-nav' stage_nav_selector = '.stage-nav'
...@@ -21,6 +23,9 @@ describe 'Group Value Stream Analytics', :js do ...@@ -21,6 +23,9 @@ describe 'Group Value Stream Analytics', :js do
before do before do
stub_licensed_features(cycle_analytics_for_groups: true) stub_licensed_features(cycle_analytics_for_groups: true)
# chart returns an error since theres no data
stub_feature_flags(Gitlab::Analytics::TASKS_BY_TYPE_CHART_FEATURE_FLAG => false)
group.add_owner(user) group.add_owner(user)
project.add_maintainer(user) project.add_maintainer(user)
...@@ -224,6 +229,7 @@ describe 'Group Value Stream Analytics', :js do ...@@ -224,6 +229,7 @@ describe 'Group Value Stream Analytics', :js do
context 'enabled' do context 'enabled' do
before do before do
stub_licensed_features(cycle_analytics_for_groups: true, type_of_work_analytics: true) stub_licensed_features(cycle_analytics_for_groups: true, type_of_work_analytics: true)
stub_feature_flags(Gitlab::Analytics::TASKS_BY_TYPE_CHART_FEATURE_FLAG => true)
sign_in(user) sign_in(user)
end end
...@@ -287,8 +293,11 @@ describe 'Group Value Stream Analytics', :js do ...@@ -287,8 +293,11 @@ describe 'Group Value Stream Analytics', :js do
describe 'Customizable cycle analytics', :js do describe 'Customizable cycle analytics', :js do
custom_stage_name = "Cool beans" custom_stage_name = "Cool beans"
custom_stage_with_labels_name = "Cool beans - now with labels"
start_event_identifier = :merge_request_created start_event_identifier = :merge_request_created
end_event_identifier = :merge_request_merged end_event_identifier = :merge_request_merged
start_label_event = :issue_label_added
stop_label_event = :issue_label_removed
let(:button_class) { '.js-add-stage-button' } let(:button_class) { '.js-add-stage-button' }
let(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } } let(:params) { { name: custom_stage_name, start_event_identifier: start_event_identifier, end_event_identifier: end_event_identifier } }
...@@ -309,6 +318,15 @@ describe 'Group Value Stream Analytics', :js do ...@@ -309,6 +318,15 @@ describe 'Group Value Stream Analytics', :js do
page.find("select[name='#{name}']").all(elem)[index].select_option page.find("select[name='#{name}']").all(elem)[index].select_option
end end
def select_dropdown_option_by_value(name, value, elem = "option")
page.find("select[name='#{name}']").find("#{elem}[value=#{value}]").select_option
end
def select_dropdown_label(field, index = 2)
page.find("[name=#{field}] .dropdown-toggle").click
page.find("[name=#{field}] .dropdown-menu").all('.dropdown-item')[index].click
end
context 'enabled' do context 'enabled' do
before do before do
select_group select_group
...@@ -340,10 +358,6 @@ describe 'Group Value Stream Analytics', :js do ...@@ -340,10 +358,6 @@ describe 'Group Value Stream Analytics', :js do
context 'Custom stage form' do context 'Custom stage form' do
let(:show_form_button_class) { '.js-add-stage-button' } let(:show_form_button_class) { '.js-add-stage-button' }
def select_dropdown_option(name, elem = "option", index = 1)
page.find("select[name='#{name}']").all(elem)[index].select_option
end
before do before do
select_group select_group
...@@ -357,6 +371,39 @@ describe 'Group Value Stream Analytics', :js do ...@@ -357,6 +371,39 @@ describe 'Group Value Stream Analytics', :js do
end end
end end
shared_examples 'submits the form successfully' do |stage_name|
it 'submit button is enabled' do
expect(page).to have_button('Add stage', disabled: false)
end
it 'submit button is disabled if the start event changes' do
select_dropdown_option 'custom-stage-start-event', 'option', 2
expect(page).to have_button('Add stage', disabled: true)
end
it 'the custom stage is saved' do
click_button 'Add stage'
expect(page).to have_selector('.stage-nav-item', text: stage_name)
end
it 'a confirmation message is displayed' do
fill_in 'custom-stage-name', with: stage_name
click_button 'Add stage'
expect(page.find('.flash-notice')).to have_text("Your custom stage '#{stage_name}' was created")
end
it 'with a default name' do
name = 'issue'
fill_in 'custom-stage-name', with: name
click_button 'Add stage'
expect(page.find('.flash-alert')).to have_text("'#{name}' stage already exists")
end
end
context 'with all required fields set' do context 'with all required fields set' do
before do before do
fill_in 'custom-stage-name', with: custom_stage_name fill_in 'custom-stage-name', with: custom_stage_name
...@@ -364,44 +411,48 @@ describe 'Group Value Stream Analytics', :js do ...@@ -364,44 +411,48 @@ describe 'Group Value Stream Analytics', :js do
select_dropdown_option 'custom-stage-stop-event' select_dropdown_option 'custom-stage-stop-event'
end end
it 'submit button is enabled' do it 'does not have label dropdowns' do
expect(page).to have_button('Add stage', disabled: false) expect(page).not_to have_content('Start event label')
expect(page).not_to have_content('Stop event label')
end end
it 'submit button is disabled if the start event changes' do it_behaves_like 'submits the form successfully', custom_stage_name
select_dropdown_option 'custom-stage-start-event', 'option', 2 end
expect(page).to have_button('Add stage', disabled: true) context 'with label based stages selected' do
before do
fill_in 'custom-stage-name', with: custom_stage_with_labels_name
select_dropdown_option_by_value 'custom-stage-start-event', start_label_event
select_dropdown_option_by_value 'custom-stage-stop-event', stop_label_event
end end
it 'an error message is displayed if the start event is changed' do it 'has label dropdowns' do
select_dropdown_option 'custom-stage-start-event', 'option', 2 expect(page).to have_content('Start event label')
expect(page).to have_content('Stop event label')
end
expect(page).to have_text 'Start event changed, please select a valid stop event' it 'submit button is disabled' do
expect(page).to have_button('Add stage', disabled: true)
end end
context 'submit button is clicked' do it 'does not contain labels from outside the group' do
it 'the custom stage is saved' do field = 'custom-stage-start-event-label'
click_button 'Add stage' page.find("[name=#{field}] .dropdown-toggle").click
expect(page).to have_selector('.stage-nav-item', text: custom_stage_name) menu = page.find("[name=#{field}] .dropdown-menu")
end
it 'a confirmation message is displayed' do expect(menu).not_to have_content(label3.name)
name = 'cool beans number 2' expect(menu).to have_content(label.name)
fill_in 'custom-stage-name', with: name expect(menu).to have_content(label2.name)
click_button 'Add stage' end
expect(page.find('.flash-notice')).to have_text("Your custom stage '#{name}' was created") context 'with all required fields set' do
before do
select_dropdown_label 'custom-stage-start-event-label', 1
select_dropdown_label 'custom-stage-stop-event-label', 2
end end
it 'with a default name' do it_behaves_like 'submits the form successfully', custom_stage_with_labels_name
name = 'issue'
fill_in 'custom-stage-name', with: name
click_button 'Add stage'
expect(page.find('.flash-alert')).to have_text("'#{name}' stage already exists")
end
end end
end end
end end
......
...@@ -6,7 +6,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display ...@@ -6,7 +6,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display
</button>" </button>"
`; `;
exports[`CustomStageForm Empty form Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = ` exports[`CustomStageForm Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__123\\"> "<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__123\\">
<option value=\\"\\">Select start event</option> <option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option> <option value=\\"issue_created\\">Issue created</option>
...@@ -29,7 +29,7 @@ exports[`CustomStageForm Empty form Start event with events does not select even ...@@ -29,7 +29,7 @@ exports[`CustomStageForm Empty form Start event with events does not select even
</select>" </select>"
`; `;
exports[`CustomStageForm Empty form Start event with events selects events with canBeStartEvent=true for the start events dropdown 1`] = ` exports[`CustomStageForm Start event with events selects events with canBeStartEvent=true for the start events dropdown 1`] = `
"<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__95\\"> "<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__95\\">
<option value=\\"\\">Select start event</option> <option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option> <option value=\\"issue_created\\">Issue created</option>
...@@ -52,7 +52,7 @@ exports[`CustomStageForm Empty form Start event with events selects events with ...@@ -52,7 +52,7 @@ exports[`CustomStageForm Empty form Start event with events selects events with
</select>" </select>"
`; `;
exports[`CustomStageForm Empty form isSavingCustomStage=true displays a loading icon 1`] = ` exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = `
"<button disabled=\\"disabled\\" type=\\"button\\" class=\\"js-save-stage btn btn-success\\"><span class=\\"gl-spinner-container\\"><span aria-label=\\"Loading\\" aria-hidden=\\"true\\" class=\\"gl-spinner gl-spinner-orange gl-spinner-sm\\"></span></span> "<button disabled=\\"disabled\\" type=\\"button\\" class=\\"js-save-stage btn btn-success\\"><span class=\\"gl-spinner-container\\"><span aria-label=\\"Loading\\" aria-hidden=\\"true\\" class=\\"gl-spinner gl-spinner-orange gl-spinner-sm\\"></span></span>
Add stage Add stage
</button>" </button>"
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
labelStopEvent, labelStopEvent,
customStageStartEvents as startEvents, customStageStartEvents as startEvents,
customStageStopEvents as stopEvents, customStageStopEvents as stopEvents,
customStageFormErrors,
} from '../mock_data'; } from '../mock_data';
const initData = { const initData = {
...@@ -56,483 +57,462 @@ describe('CustomStageForm', () => { ...@@ -56,483 +57,462 @@ describe('CustomStageForm', () => {
getDropdownOption(_wrapper, dropdown, index).setSelected(); getDropdownOption(_wrapper, dropdown, index).setSelected();
} }
describe('Empty form', () => { function setEventDropdowns({ startEventDropdownIndex = 1, stopEventDropdownIndex = 1 } = {}) {
beforeEach(() => { selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex);
wrapper = createComponent({}, false); return Vue.nextTick().then(() => {
selectDropdownOption(wrapper, sel.endEvent, stopEventDropdownIndex);
}); });
}
afterEach(() => { beforeEach(() => {
wrapper.destroy(); wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
});
describe.each([
['Name', sel.name, true],
['Start event', sel.startEvent, true],
['Stop event', sel.endEvent, false],
['Submit', sel.submit, false],
['Cancel', sel.cancel, false],
])('Default state', (field, $sel, enabledState) => {
const state = enabledState ? 'enabled' : 'disabled';
it(`field '${field}' is ${state}`, () => {
const el = wrapper.find($sel);
expect(el.exists()).toEqual(true);
if (!enabledState) {
expect(el.attributes('disabled')).toEqual('disabled');
} else {
expect(el.attributes('disabled')).toBeUndefined();
}
}); });
});
describe.each([ describe('Start event', () => {
['Name', sel.name, true], describe('with events', () => {
['Start event', sel.startEvent, true], beforeEach(() => {
['Stop event', sel.endEvent, false], wrapper = createComponent({});
['Submit', sel.submit, false], });
['Cancel', sel.cancel, false],
])('by default', (field, $sel, enabledState) => {
const state = enabledState ? 'enabled' : 'disabled';
it(`field '${field}' is ${state}`, () => {
const el = wrapper.find($sel);
expect(el.exists()).toEqual(true);
if (!enabledState) {
expect(el.attributes('disabled')).toEqual('disabled');
} else {
expect(el.attributes('disabled')).toBeUndefined();
}
});
});
describe('Start event', () => {
describe('with events', () => {
beforeEach(() => {
wrapper = createComponent({}, false);
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('selects events with canBeStartEvent=true for the start events dropdown', () => { it('selects events with canBeStartEvent=true for the start events dropdown', () => {
const select = wrapper.find(sel.startEvent); const select = wrapper.find(sel.startEvent);
expect(select.html()).toMatchSnapshot(); expect(select.html()).toMatchSnapshot();
}); });
it('does not select events with canBeStartEvent=false for the start events dropdown', () => { it('does not select events with canBeStartEvent=false for the start events dropdown', () => {
const select = wrapper.find(sel.startEvent); const select = wrapper.find(sel.startEvent);
expect(select.html()).toMatchSnapshot(); expect(select.html()).toMatchSnapshot();
stopEvents stopEvents
.filter(ev => !ev.canBeStartEvent) .filter(ev => !ev.canBeStartEvent)
.forEach(ev => { .forEach(ev => {
expect(select.html()).not.toHaveHtml( expect(select.html()).not.toHaveHtml(
`<option value="${ev.identifier}">${ev.name}</option>`, `<option value="${ev.identifier}">${ev.name}</option>`,
); );
}); });
});
}); });
});
describe('start event label', () => { describe('start event label', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({}, false); wrapper = createComponent({}, false);
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('is hidden by default', () => { it('is hidden by default', () => {
expect(wrapper.find(sel.startEventLabel).exists()).toEqual(false); expect(wrapper.find(sel.startEventLabel).exists()).toEqual(false);
}); });
it('will display the start event label field if a label event is selected', done => { it('will display the start event label field if a label event is selected', () => {
wrapper.setData({ wrapper.setData({
fields: { fields: {
startEventIdentifier: labelStartEvent.identifier, startEventIdentifier: labelStartEvent.identifier,
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(wrapper.find(sel.startEventLabel).exists()).toEqual(true); expect(wrapper.find(sel.startEventLabel).exists()).toEqual(true);
done();
});
}); });
});
it('will set the "startEventLabelId" field when selected', done => { it('will set the "startEventLabelId" field when selected', () => {
const selectedLabelId = groupLabels[0].id; const selectedLabelId = groupLabels[0].id;
expect(wrapper.vm.fields.startEventLabelId).toEqual(null); expect(wrapper.vm.fields.startEventLabelId).toEqual(null);
wrapper.find(sel.startEvent).setValue(labelStartEvent.identifier); wrapper.find(sel.startEvent).setValue(labelStartEvent.identifier);
Vue.nextTick(() => { // TODO: make func for setting single field
return Vue.nextTick()
.then(() => {
wrapper wrapper
.find(sel.startEventLabel) .find(sel.startEventLabel)
.findAll('.dropdown-item') .findAll('.dropdown-item')
.at(1) // item at index 0 is 'select a label' .at(1) // item at index 0 is 'select a label'
.trigger('click'); .trigger('click');
Vue.nextTick(() => { return Vue.nextTick();
expect(wrapper.vm.fields.startEventLabelId).toEqual(selectedLabelId); })
done(); .then(() => {
}); expect(wrapper.vm.fields.startEventLabelId).toEqual(selectedLabelId);
}); });
});
}); });
}); });
});
describe('Stop event', () => { describe('Stop event', () => {
const startEventArrayIndex = 2; const startEventArrayIndex = 2;
const startEventDropdownIndex = 1; const startEventDropdownIndex = 1;
const currAllowed = startEvents[startEventArrayIndex].allowedEndEvents; const currAllowed = startEvents[startEventArrayIndex].allowedEndEvents;
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({}, false); wrapper = createComponent({}, false);
}); });
it('notifies that a start event needs to be selected first', () => { it('notifies that a start event needs to be selected first', () => {
expect(wrapper.text()).toContain('Please select a start event first'); expect(wrapper.text()).toContain('Please select a start event first');
}); });
it('clears notification when a start event is selected', done => { it('clears notification when a start event is selected', () => {
selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex); selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex);
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(wrapper.text()).not.toContain('Please select a start event first'); expect(wrapper.text()).not.toContain('Please select a start event first');
done();
});
}); });
});
it('is enabled when a start event is selected', done => { it('is enabled when a start event is selected', () => {
const el = wrapper.find(sel.endEvent); const el = wrapper.find(sel.endEvent);
expect(el.attributes('disabled')).toEqual('disabled'); expect(el.attributes('disabled')).toEqual('disabled');
selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex); selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex);
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(el.attributes('disabled')).toBeUndefined(); expect(el.attributes('disabled')).toBeUndefined();
done();
});
}); });
});
it('will update the list of stop events when a start event is changed', done => { it('will update the list of stop events when a start event is changed', () => {
let stopOptions = wrapper.find(sel.endEvent).findAll('option'); let stopOptions = wrapper.find(sel.endEvent).findAll('option');
const selectedStartEvent = startEvents[startEventDropdownIndex]; const selectedStartEvent = startEvents[startEventDropdownIndex];
expect(stopOptions.length).toEqual(1); expect(stopOptions.length).toEqual(1);
selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex); selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex);
Vue.nextTick(() => { return Vue.nextTick().then(() => {
stopOptions = wrapper.find(sel.endEvent); stopOptions = wrapper.find(sel.endEvent);
selectedStartEvent.allowedEndEvents.forEach(identifier => { selectedStartEvent.allowedEndEvents.forEach(identifier => {
expect(stopOptions.html()).toContain(identifier); expect(stopOptions.html()).toContain(identifier);
});
done();
}); });
}); });
});
it('will display all the valid stop events', done => { it('will display all the valid stop events', () => {
let stopOptions = wrapper.find(sel.endEvent).findAll('option'); let stopOptions = wrapper.find(sel.endEvent).findAll('option');
const possibleEndEvents = stopEvents.filter(ev => currAllowed.includes(ev.identifier)); const possibleEndEvents = stopEvents.filter(ev => currAllowed.includes(ev.identifier));
expect(stopOptions.at(0).html()).toEqual('<option value="">Select stop event</option>'); expect(stopOptions.at(0).html()).toEqual('<option value="">Select stop event</option>');
selectDropdownOption(wrapper, sel.startEvent, startEventArrayIndex + 1); selectDropdownOption(wrapper, sel.startEvent, startEventArrayIndex + 1);
Vue.nextTick(() => { return Vue.nextTick().then(() => {
stopOptions = wrapper.find(sel.endEvent); stopOptions = wrapper.find(sel.endEvent);
possibleEndEvents.forEach(({ name, identifier }) => { possibleEndEvents.forEach(({ name, identifier }) => {
expect(stopOptions.html()).toContain(`<option value="${identifier}">${name}</option>`); expect(stopOptions.html()).toContain(`<option value="${identifier}">${name}</option>`);
});
done();
}); });
}); });
});
it('will not display stop events that are not in the list of allowed stop events', done => { it('will not display stop events that are not in the list of allowed stop events', () => {
let stopOptions = wrapper.find(sel.endEvent).findAll('option'); let stopOptions = wrapper.find(sel.endEvent).findAll('option');
const excludedEndEvents = stopEvents.filter(ev => !currAllowed.includes(ev.identifier)); const excludedEndEvents = stopEvents.filter(ev => !currAllowed.includes(ev.identifier));
expect(stopOptions.at(0).html()).toEqual('<option value="">Select stop event</option>'); expect(stopOptions.at(0).html()).toEqual('<option value="">Select stop event</option>');
selectDropdownOption(wrapper, sel.startEvent, startEventArrayIndex + 1); selectDropdownOption(wrapper, sel.startEvent, startEventArrayIndex + 1);
Vue.nextTick(() => { return Vue.nextTick().then(() => {
stopOptions = wrapper.find(sel.endEvent); stopOptions = wrapper.find(sel.endEvent);
excludedEndEvents.forEach(({ name, identifier }) => { excludedEndEvents.forEach(({ name, identifier }) => {
expect(wrapper.find(sel.endEvent).html()).not.toHaveHtml( expect(wrapper.find(sel.endEvent).html()).not.toHaveHtml(
`<option value="${identifier}">${name}</option>`, `<option value="${identifier}">${name}</option>`,
); );
});
done();
}); });
}); });
});
describe('with a stop event selected and a change to the start event', () => { describe('with a stop event selected and a change to the start event', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({}, false); wrapper = createComponent({});
wrapper.setData({
fields: {
name: 'Cool stage',
startEventIdentifier: 'issue_created',
startEventLabelId: null,
endEventIdentifier: 'issue_stage_end',
endEventLabelId: null,
},
});
});
afterEach(() => { wrapper.setData({
wrapper.destroy(); fields: {
name: 'Cool stage',
startEventIdentifier: 'issue_created',
startEventLabelId: null,
endEventIdentifier: 'issue_stage_end',
endEventLabelId: null,
},
}); });
});
it('will notify if the current start and stop event pair is not valid', done => { afterEach(() => {
expect(wrapper.find(sel.invalidFeedback).exists()).toEqual(false); wrapper.destroy();
});
selectDropdownOption(wrapper, sel.startEvent, 2); it('will notify if the current start and stop event pair is not valid', () => {
selectDropdownOption(wrapper, sel.startEvent, 2);
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(wrapper.find(sel.invalidFeedback).exists()).toEqual(true); expect(wrapper.find(sel.invalidFeedback).exists()).toEqual(true);
expect(wrapper.find(sel.invalidFeedback).text()).toContain( expect(wrapper.find(sel.invalidFeedback).text()).toContain(
'Start event changed, please select a valid stop event', 'Start event changed, please select a valid stop event',
); );
done();
});
}); });
});
it('will update the list of stop events', done => { it('will update the list of stop events', () => {
const se = wrapper.vm.endEventOptions; const se = wrapper.vm.endEventOptions;
selectDropdownOption(wrapper, sel.startEvent, 2); selectDropdownOption(wrapper, sel.startEvent, 2);
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(se[1].value).not.toEqual(wrapper.vm.endEventOptions[1].value); expect(se[1].value).not.toEqual(wrapper.vm.endEventOptions[1].value);
done();
});
}); });
});
it('will disable the submit button until a valid endEvent is selected', done => { it('will disable the submit button until a valid endEvent is selected', () => {
selectDropdownOption(wrapper, sel.startEvent, 2); selectDropdownOption(wrapper, sel.startEvent, 2);
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(wrapper.find(sel.submit).attributes('disabled')).toEqual('disabled'); expect(wrapper.find(sel.submit).attributes('disabled')).toEqual('disabled');
done();
});
}); });
}); });
});
describe('Stop event label', () => { describe('Stop event label', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({}, false); wrapper = createComponent({});
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('is hidden by default', () => { it('is hidden by default', () => {
expect(wrapper.find(sel.startEventLabel).exists()).toEqual(false); expect(wrapper.find(sel.startEventLabel).exists()).toEqual(false);
}); });
it('will display the stop event label field if a label event is selected', done => { it('will display the stop event label field if a label event is selected', () => {
expect(wrapper.find(sel.endEventLabel).exists()).toEqual(false); expect(wrapper.find(sel.endEventLabel).exists()).toEqual(false);
wrapper.setData({ wrapper.setData({
fields: { fields: {
endEventIdentifier: labelStopEvent.identifier, endEventIdentifier: labelStopEvent.identifier,
startEventIdentifier: labelStartEvent.identifier, startEventIdentifier: labelStartEvent.identifier,
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(wrapper.find(sel.endEventLabel).exists()).toEqual(true); expect(wrapper.find(sel.endEventLabel).exists()).toEqual(true);
done();
});
}); });
});
it('will set the "endEventLabelId" field when selected', done => { it('will set the "endEventLabelId" field when selected', () => {
const selectedLabelId = groupLabels[1].id; const selectedLabelId = groupLabels[1].id;
expect(wrapper.vm.fields.endEventLabelId).toEqual(null); expect(wrapper.vm.fields.endEventLabelId).toEqual(null);
wrapper.setData({ wrapper.setData({
fields: { fields: {
startEventIdentifier: labelStartEvent.identifier, startEventIdentifier: labelStartEvent.identifier,
endEventIdentifier: labelStopEvent.identifier, endEventIdentifier: labelStopEvent.identifier,
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick()
.then(() => {
wrapper wrapper
.find(sel.endEventLabel) .find(sel.endEventLabel)
.findAll('.dropdown-item') .findAll('.dropdown-item')
.at(2) // item at index 0 is 'select a label' .at(2) // item at index 0 is 'select a label'
.trigger('click'); .trigger('click');
Vue.nextTick(() => { return Vue.nextTick();
expect(wrapper.vm.fields.endEventLabelId).toEqual(selectedLabelId); })
done(); .then(() => {
}); expect(wrapper.vm.fields.endEventLabelId).toEqual(selectedLabelId);
}); });
});
}); });
}); });
});
describe('Add stage button', () => { describe('Add stage button', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({}, false); wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
});
selectDropdownOption(wrapper, sel.startEvent, 1); it('has text `Add stage`', () => {
expect(wrapper.find(sel.submit).text()).toEqual('Add stage');
});
return Vue.nextTick(() => { it('is enabled when all required fields are filled', () => {
selectDropdownOption(wrapper, sel.endEvent, 1); const btn = wrapper.find(sel.submit);
return Vue.nextTick();
});
});
afterEach(() => { expect(btn.attributes('disabled')).toEqual('disabled');
wrapper.destroy(); wrapper.find(sel.name).setValue('Cool stage');
});
it('has text `Add stage`', () => { return setEventDropdowns().then(() => {
expect(wrapper.find(sel.submit).text('value')).toEqual('Add stage'); expect(btn.attributes('disabled')).toBeUndefined();
}); });
});
it('is enabled when all required fields are filled', done => { describe('with all fields set', () => {
const btn = wrapper.find(sel.submit); const startEventDropdownIndex = 2;
const startEventArrayIndex = startEventDropdownIndex - 1;
const stopEventDropdownIndex = 1;
expect(btn.attributes('disabled')).toEqual('disabled'); beforeEach(() => {
wrapper = createComponent({});
wrapper.find(sel.name).setValue('Cool stage'); wrapper.find(sel.name).setValue('Cool stage');
return Vue.nextTick().then(() =>
Vue.nextTick(() => { setEventDropdowns({ startEventDropdownIndex, stopEventDropdownIndex }),
expect(btn.attributes('disabled')).toBeUndefined(); );
done();
});
}); });
describe('with all fields set', () => { afterEach(() => {
const startEventDropdownIndex = 2; wrapper.destroy();
const startEventArrayIndex = startEventDropdownIndex - 1; });
const stopEventIndex = 1;
beforeEach(() => {
wrapper = createComponent({}, false);
selectDropdownOption(wrapper, sel.startEvent, startEventDropdownIndex); it(`emits a ${STAGE_ACTIONS.CREATE} event when clicked`, () => {
let event = findEvent(STAGE_ACTIONS.CREATE);
expect(event).toBeUndefined();
return Vue.nextTick() wrapper.find(sel.submit).trigger('click');
.then(() => {
selectDropdownOption(wrapper, sel.endEvent, stopEventIndex);
return Vue.nextTick();
})
.then(() => {
wrapper.find(sel.name).setValue('Cool stage');
return Vue.nextTick();
});
});
afterEach(() => { return Vue.nextTick().then(() => {
wrapper.destroy(); event = findEvent(STAGE_ACTIONS.CREATE);
expect(event).toBeTruthy();
expect(event.length).toEqual(1);
}); });
});
it(`emits a ${STAGE_ACTIONS.CREATE} event when clicked`, done => { it(`${STAGE_ACTIONS.CREATE} event receives the latest data`, () => {
let event = findEvent(STAGE_ACTIONS.CREATE); const startEv = startEvents[startEventArrayIndex];
expect(event).toBeUndefined(); const selectedStopEvent = getDropdownOption(wrapper, sel.endEvent, stopEventDropdownIndex);
let event = findEvent(STAGE_ACTIONS.CREATE);
wrapper.find(sel.submit).trigger('click'); expect(event).toBeUndefined();
Vue.nextTick(() => { const res = [
event = findEvent(STAGE_ACTIONS.CREATE); {
expect(event).toBeTruthy(); id: null,
expect(event.length).toEqual(1); name: 'Cool stage',
done(); start_event_identifier: startEv.identifier,
}); start_event_label_id: null,
}); end_event_identifier: selectedStopEvent.attributes('value'),
end_event_label_id: null,
},
];
it(`${STAGE_ACTIONS.CREATE} event receives the latest data`, () => { wrapper.find(sel.submit).trigger('click');
const startEv = startEvents[startEventArrayIndex]; return Vue.nextTick().then(() => {
const selectedStopEvent = getDropdownOption(wrapper, sel.endEvent, stopEventIndex); event = findEvent(STAGE_ACTIONS.CREATE);
let event = findEvent(STAGE_ACTIONS.CREATE); expect(event[0]).toEqual(res);
expect(event).toBeUndefined();
const res = [
{
id: null,
name: 'Cool stage',
start_event_identifier: startEv.identifier,
start_event_label_id: null,
end_event_identifier: selectedStopEvent.attributes('value'),
end_event_label_id: null,
},
];
wrapper.find(sel.submit).trigger('click');
return Vue.nextTick().then(() => {
event = findEvent(STAGE_ACTIONS.CREATE);
expect(event[0]).toEqual(res);
});
}); });
}); });
}); });
});
describe('Cancel button', () => { describe('Cancel button', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({}, false); wrapper = createComponent({});
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('is enabled when the form is dirty', done => { it('is enabled when the form is dirty', () => {
const btn = wrapper.find(sel.cancel); const btn = wrapper.find(sel.cancel);
expect(btn.attributes('disabled')).toEqual('disabled'); expect(btn.attributes('disabled')).toEqual('disabled');
wrapper.find(sel.name).setValue('Cool stage'); wrapper.find(sel.name).setValue('Cool stage');
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(btn.attributes('disabled')).toBeUndefined(); expect(btn.attributes('disabled')).toBeUndefined();
done();
});
}); });
});
it('will reset the fields when clicked', done => { it('will reset the fields when clicked', () => {
wrapper.setData({ wrapper.setData({
fields: { fields: {
name: 'Cool stage pre', name: 'Cool stage pre',
startEventIdentifier: labelStartEvent.identifier, startEventIdentifier: labelStartEvent.identifier,
endEventIdentifier: labelStopEvent.identifier, endEventIdentifier: labelStopEvent.identifier,
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick()
.then(() => {
wrapper.find(sel.cancel).trigger('click'); wrapper.find(sel.cancel).trigger('click');
Vue.nextTick(() => { return Vue.nextTick();
expect(wrapper.vm.fields).toEqual({ })
id: null, .then(() => {
name: null, expect(wrapper.vm.fields).toEqual({
startEventIdentifier: null, id: null,
startEventLabelId: null, name: null,
endEventIdentifier: null, startEventIdentifier: null,
endEventLabelId: null, startEventLabelId: null,
}); endEventIdentifier: null,
done(); endEventLabelId: null,
}); });
}); });
}); });
it('will emit the `cancel` event when clicked', done => { it('will emit the `cancel` event when clicked', () => {
let ev = findEvent('cancel'); let ev = findEvent('cancel');
expect(ev).toBeUndefined(); expect(ev).toBeUndefined();
wrapper.setData({ wrapper.setData({
fields: { fields: {
name: 'Cool stage pre', name: 'Cool stage pre',
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick()
.then(() => {
wrapper.find(sel.cancel).trigger('click'); wrapper.find(sel.cancel).trigger('click');
return Vue.nextTick();
Vue.nextTick(() => { })
ev = findEvent('cancel'); .then(() => {
expect(ev).toBeTruthy(); ev = findEvent('cancel');
expect(ev.length).toEqual(1); expect(ev).toBeTruthy();
done(); expect(ev.length).toEqual(1);
});
}); });
});
}); });
});
describe('isSavingCustomStage=true', () => { describe('isSavingCustomStage=true', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent( wrapper = createComponent(
{ {
isSavingCustomStage: true, isSavingCustomStage: true,
}, },
false, false,
); );
}); });
it('displays a loading icon', () => { it('displays a loading icon', () => {
expect(wrapper.find(sel.submit).html()).toMatchSnapshot(); expect(wrapper.find(sel.submit).html()).toMatchSnapshot();
});
}); });
}); });
...@@ -562,7 +542,7 @@ describe('CustomStageForm', () => { ...@@ -562,7 +542,7 @@ describe('CustomStageForm', () => {
}); });
describe('Cancel button', () => { describe('Cancel button', () => {
it('will reset the fields to initial state when clicked', done => { it('will reset the fields to initial state when clicked', () => {
wrapper.setData({ wrapper.setData({
fields: { fields: {
name: 'Cool stage pre', name: 'Cool stage pre',
...@@ -571,16 +551,14 @@ describe('CustomStageForm', () => { ...@@ -571,16 +551,14 @@ describe('CustomStageForm', () => {
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick()
wrapper.find(sel.cancel).trigger('click'); .then(() => {
wrapper.find(sel.cancel).trigger('click');
Vue.nextTick(() => { return Vue.nextTick();
expect(wrapper.vm.fields).toEqual({ })
...initData, .then(() => {
}); expect(wrapper.vm.fields).toEqual({ ...initData });
done();
}); });
});
}); });
}); });
...@@ -593,33 +571,31 @@ describe('CustomStageForm', () => { ...@@ -593,33 +571,31 @@ describe('CustomStageForm', () => {
expect(wrapper.find(sel.submit).attributes('disabled')).toEqual('disabled'); expect(wrapper.find(sel.submit).attributes('disabled')).toEqual('disabled');
}); });
it('is enabled when a field is changed and fields are valid', done => { it('is enabled when a field is changed and fields are valid', () => {
wrapper.setData({ wrapper.setData({
fields: { fields: {
name: 'Cool updated form', name: 'Cool updated form',
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(wrapper.find(sel.submit).attributes('disabled')).toBeUndefined(); expect(wrapper.find(sel.submit).attributes('disabled')).toBeUndefined();
done();
}); });
}); });
it('is disabled when a field is changed but fields are incomplete', done => { it('is disabled when a field is changed but fields are incomplete', () => {
wrapper.setData({ wrapper.setData({
fields: { fields: {
name: '', name: '',
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick().then(() => {
expect(wrapper.find(sel.submit).attributes('disabled')).toEqual('disabled'); expect(wrapper.find(sel.submit).attributes('disabled')).toEqual('disabled');
done();
}); });
}); });
it(`emits a ${STAGE_ACTIONS.UPDATE} event when clicked`, done => { it(`emits a ${STAGE_ACTIONS.UPDATE} event when clicked`, () => {
let ev = findEvent(STAGE_ACTIONS.UPDATE); let ev = findEvent(STAGE_ACTIONS.UPDATE);
expect(ev).toBeUndefined(); expect(ev).toBeUndefined();
...@@ -629,29 +605,31 @@ describe('CustomStageForm', () => { ...@@ -629,29 +605,31 @@ describe('CustomStageForm', () => {
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick()
wrapper.find(sel.submit).trigger('click'); .then(() => {
wrapper.find(sel.submit).trigger('click');
Vue.nextTick(() => { return Vue.nextTick();
})
.then(() => {
ev = findEvent(STAGE_ACTIONS.UPDATE); ev = findEvent(STAGE_ACTIONS.UPDATE);
expect(ev).toBeTruthy(); expect(ev).toBeTruthy();
expect(ev.length).toEqual(1); expect(ev.length).toEqual(1);
done();
}); });
});
}); });
it('`submit` event receives the latest data', done => { it('`submit` event receives the latest data', () => {
wrapper.setData({ wrapper.setData({
fields: { fields: {
name: 'Cool updated form', name: 'Cool updated form',
}, },
}); });
Vue.nextTick(() => { return Vue.nextTick()
wrapper.find(sel.submit).trigger('click'); .then(() => {
wrapper.find(sel.submit).trigger('click');
Vue.nextTick(() => { return Vue.nextTick();
})
.then(() => {
const submitted = findEvent(STAGE_ACTIONS.UPDATE)[0]; const submitted = findEvent(STAGE_ACTIONS.UPDATE)[0];
expect(submitted).not.toEqual([initData]); expect(submitted).not.toEqual([initData]);
expect(submitted).toEqual([ expect(submitted).toEqual([
...@@ -664,30 +642,44 @@ describe('CustomStageForm', () => { ...@@ -664,30 +642,44 @@ describe('CustomStageForm', () => {
name: 'Cool updated form', name: 'Cool updated form',
}, },
]); ]);
done();
}); });
});
}); });
}); });
describe('isSavingCustomStage=true', () => { describe('isSavingCustomStage=true', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent( wrapper = createComponent({
{ isEditingCustomStage: true,
isEditingCustomStage: true, initialFields: {
initialFields: { ...initData,
...initData,
},
isSavingCustomStage: true,
}, },
false, isSavingCustomStage: true,
); });
}); });
it('displays a loading icon', () => { it('displays a loading icon', () => {
expect(wrapper.find(sel.submit).html()).toMatchSnapshot(); expect(wrapper.find(sel.submit).html()).toMatchSnapshot();
}); });
}); });
}); });
describe('With errors', () => {
beforeEach(() => {
wrapper = createComponent({
initialFields: initData,
errors: customStageFormErrors,
});
return Vue.nextTick();
});
afterEach(() => {
wrapper.destroy();
});
it('renders the errors for the relevant fields', () => {
expect(wrapper.find({ ref: 'name' }).html()).toContain('is reserved');
expect(wrapper.find({ ref: 'name' }).html()).toContain('cant be blank');
expect(wrapper.find({ ref: 'startEventIdentifier' }).html()).toContain('cant be blank');
});
});
}); });
...@@ -118,6 +118,13 @@ export const labelStopEvent = customStageLabelEvents.find( ...@@ -118,6 +118,13 @@ export const labelStopEvent = customStageLabelEvents.find(
ev => ev.identifier === labelStartEvent.allowedEndEvents[0], ev => ev.identifier === labelStartEvent.allowedEndEvents[0],
); );
export const rawCustomStageFormErrors = {
name: ['is reserved', 'cant be blank'],
start_event_identifier: ['cant be blank'],
};
export const customStageFormErrors = convertObjectPropsToCamelCase(rawCustomStageFormErrors);
const dateRange = getDatesInRange(startDate, endDate, toYmd); const dateRange = getDatesInRange(startDate, endDate, toYmd);
export const tasksByTypeData = getJSONFixture('analytics/type_of_work/tasks_by_type.json').map( export const tasksByTypeData = getJSONFixture('analytics/type_of_work/tasks_by_type.json').map(
......
...@@ -6,6 +6,7 @@ import * as actions from 'ee/analytics/cycle_analytics/store/actions'; ...@@ -6,6 +6,7 @@ import * as actions from 'ee/analytics/cycle_analytics/store/actions';
import * as types from 'ee/analytics/cycle_analytics/store/mutation_types'; import * as types from 'ee/analytics/cycle_analytics/store/mutation_types';
import { TASKS_BY_TYPE_FILTERS } from 'ee/analytics/cycle_analytics/constants'; import { TASKS_BY_TYPE_FILTERS } from 'ee/analytics/cycle_analytics/constants';
import createFlash from '~/flash'; import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { import {
group, group,
summaryData, summaryData,
...@@ -21,7 +22,7 @@ import { ...@@ -21,7 +22,7 @@ import {
} from '../mock_data'; } from '../mock_data';
const stageData = { events: [] }; const stageData = { events: [] };
const error = new Error('Request failed with status code 404'); const error = new Error(`Request failed with status code ${httpStatusCodes.NOT_FOUND}`);
const flashErrorMessage = 'There was an error while fetching value stream analytics data.'; const flashErrorMessage = 'There was an error while fetching value stream analytics data.';
const selectedGroup = { fullPath: group.path }; const selectedGroup = { fullPath: group.path };
const [selectedStage] = stages; const [selectedStage] = stages;
...@@ -125,7 +126,7 @@ describe('Cycle analytics actions', () => { ...@@ -125,7 +126,7 @@ describe('Cycle analytics actions', () => {
describe('with a failing request', () => { describe('with a failing request', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(endpoints.stageData).replyOnce(404, { error }); mock.onGet(endpoints.stageData).replyOnce(httpStatusCodes.NOT_FOUND, { error });
}); });
it('dispatches receiveStageDataError on error', done => { it('dispatches receiveStageDataError on error', done => {
...@@ -620,12 +621,13 @@ describe('Cycle analytics actions', () => { ...@@ -620,12 +621,13 @@ describe('Cycle analytics actions', () => {
beforeEach(() => { beforeEach(() => {
setFixtures('<div class="flash-container"></div>'); setFixtures('<div class="flash-container"></div>');
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onPut(stageEndpoint({ stageId })).replyOnce(404); mock.onPut(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.NOT_FOUND);
}); });
it('dispatches receiveUpdateStageError', done => { it('dispatches receiveUpdateStageError', done => {
const data = { const data = {
id: stageId, id: stageId,
name: 'issue',
...payload, ...payload,
}; };
testAction( testAction(
...@@ -637,7 +639,10 @@ describe('Cycle analytics actions', () => { ...@@ -637,7 +639,10 @@ describe('Cycle analytics actions', () => {
{ type: 'requestUpdateStage' }, { type: 'requestUpdateStage' },
{ {
type: 'receiveUpdateStageError', type: 'receiveUpdateStageError',
payload: { error, data }, payload: {
status: httpStatusCodes.NOT_FOUND,
data,
},
}, },
], ],
done, done,
...@@ -651,13 +656,9 @@ describe('Cycle analytics actions', () => { ...@@ -651,13 +656,9 @@ describe('Cycle analytics actions', () => {
state, state,
}, },
{ {
error: { status: httpStatusCodes.UNPROCESSABLE_ENTITY,
response: { responseData: {
status: 422, errors: { name: ['is reserved'] },
data: {
errors: { name: ['is reserved'] },
},
},
}, },
data: { data: {
name: stageId, name: stageId,
...@@ -675,13 +676,60 @@ describe('Cycle analytics actions', () => { ...@@ -675,13 +676,60 @@ describe('Cycle analytics actions', () => {
commit: () => {}, commit: () => {},
state, state,
}, },
{}, { status: httpStatusCodes.BAD_REQUEST },
); );
shouldFlashAMessage('There was a problem saving your custom stage, please try again'); shouldFlashAMessage('There was a problem saving your custom stage, please try again');
done(); done();
}); });
}); });
describe('receiveUpdateStageSuccess', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
const response = {
title: 'NEW - COOL',
};
it('will dispatch fetchGroupStagesAndEvents and fetchSummaryData', () =>
testAction(
actions.receiveUpdateStageSuccess,
response,
state,
[{ type: types.RECEIVE_UPDATE_STAGE_SUCCESS }],
[{ type: 'fetchGroupStagesAndEvents' }, { type: 'setSelectedStage', payload: response }],
));
it('will flash a success message', () =>
actions
.receiveUpdateStageSuccess(
{
dispatch: () => {},
commit: () => {},
},
response,
)
.then(() => {
shouldFlashAMessage('Stage data updated');
}));
describe('with an error', () => {
it('will flash an error message', () =>
actions
.receiveUpdateStageSuccess(
{
dispatch: () => Promise.reject(),
commit: () => {},
},
response,
)
.then(() => {
shouldFlashAMessage('There was a problem refreshing the data, please try again');
}));
});
});
}); });
describe('removeStage', () => { describe('removeStage', () => {
...@@ -712,7 +760,7 @@ describe('Cycle analytics actions', () => { ...@@ -712,7 +760,7 @@ describe('Cycle analytics actions', () => {
describe('with a failed request', () => { describe('with a failed request', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onDelete(stageEndpoint({ stageId })).replyOnce(404); mock.onDelete(stageEndpoint({ stageId })).replyOnce(httpStatusCodes.NOT_FOUND);
}); });
it('dispatches receiveRemoveStageError', done => { it('dispatches receiveRemoveStageError', done => {
...@@ -1189,7 +1237,7 @@ describe('Cycle analytics actions', () => { ...@@ -1189,7 +1237,7 @@ describe('Cycle analytics actions', () => {
describe('with a failing request', () => { describe('with a failing request', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(endpoints.stageMedian).reply(404, { error }); mock.onGet(endpoints.stageMedian).reply(httpStatusCodes.NOT_FOUND, { error });
}); });
it('will dispatch receiveStageMedianValuesError', done => { it('will dispatch receiveStageMedianValuesError', done => {
...@@ -1276,4 +1324,156 @@ describe('Cycle analytics actions', () => { ...@@ -1276,4 +1324,156 @@ describe('Cycle analytics actions', () => {
); );
}); });
}); });
describe('createCustomStage', () => {
describe('with valid data', () => {
const customStageData = {
startEventIdentifier: 'start_event',
endEventIdentifier: 'end_event',
name: 'cool-new-stage',
};
beforeEach(() => {
state = { ...state, selectedGroup };
mock.onPost(endpoints.baseStagesEndpointstageData).reply(201, customStageData);
});
it(`dispatches the 'receiveCreateCustomStageSuccess' action`, () =>
testAction(
actions.createCustomStage,
customStageData,
state,
[],
[
{ type: 'requestCreateCustomStage' },
{
type: 'receiveCreateCustomStageSuccess',
payload: { data: customStageData, status: 201 },
},
],
));
});
describe('with errors', () => {
const message = 'failed';
const errors = {
endEventIdentifier: ['Cant be blank'],
};
const customStageData = {
startEventIdentifier: 'start_event',
endEventIdentifier: '',
name: 'cool-new-stage',
};
beforeEach(() => {
state = { ...state, selectedGroup };
mock
.onPost(endpoints.baseStagesEndpointstageData)
.reply(httpStatusCodes.UNPROCESSABLE_ENTITY, {
message,
errors,
});
});
it(`dispatches the 'receiveCreateCustomStageError' action`, () =>
testAction(
actions.createCustomStage,
customStageData,
state,
[],
[
{ type: 'requestCreateCustomStage' },
{
type: 'receiveCreateCustomStageError',
payload: {
data: customStageData,
errors,
message,
status: httpStatusCodes.UNPROCESSABLE_ENTITY,
},
},
],
));
});
});
describe('receiveCreateCustomStageError', () => {
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: {} } },
]));
it('will flash an error message', done => {
actions.receiveCreateCustomStageError(
{
commit: () => {},
},
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(
{
commit: () => {},
},
{
...response,
status: httpStatusCodes.UNPROCESSABLE_ENTITY,
errors: { name: ['is reserved'] },
},
);
shouldFlashAMessage("'uh oh' stage already exists");
done();
});
});
});
describe('receiveCreateCustomStageSuccess', () => {
const response = {
data: {
title: 'COOL',
},
};
it('will dispatch fetchGroupStagesAndEvents and fetchSummaryData', () =>
testAction(
actions.receiveCreateCustomStageSuccess,
response,
state,
[{ type: types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS }],
[{ type: 'fetchGroupStagesAndEvents' }, { type: 'fetchSummaryData' }],
));
describe('with an error', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('will flash an error message', () =>
actions
.receiveCreateCustomStageSuccess(
{
dispatch: () => Promise.reject(),
commit: () => {},
},
response,
)
.then(() => {
shouldFlashAMessage('There was a problem refreshing the data, please try again');
}));
});
});
}); });
...@@ -37,8 +37,15 @@ describe('Cycle analytics mutations', () => { ...@@ -37,8 +37,15 @@ describe('Cycle analytics mutations', () => {
it.each` it.each`
mutation | stateKey | value mutation | stateKey | value
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false} ${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} | ${'isCreatingCustomStage'} | ${true}
${types.EDIT_CUSTOM_STAGE} | ${'isEditingCustomStage'} | ${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.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false} ${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
...@@ -52,11 +59,20 @@ describe('Cycle analytics mutations', () => { ...@@ -52,11 +59,20 @@ describe('Cycle analytics mutations', () => {
${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'customStageFormEvents'} | ${[]} ${types.RECEIVE_GROUP_STAGES_AND_EVENTS_ERROR} | ${'customStageFormEvents'} | ${[]}
${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'customStageFormEvents'} | ${[]} ${types.REQUEST_GROUP_STAGES_AND_EVENTS} | ${'customStageFormEvents'} | ${[]}
${types.REQUEST_CREATE_CUSTOM_STAGE} | ${'isSavingCustomStage'} | ${true} ${types.REQUEST_CREATE_CUSTOM_STAGE} | ${'isSavingCustomStage'} | ${true}
${types.RECEIVE_CREATE_CUSTOM_STAGE_RESPONSE} | ${'isSavingCustomStage'} | ${false} ${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_TASKS_BY_TYPE_DATA} | ${'isLoadingTasksByTypeChart'} | ${true} ${types.REQUEST_TASKS_BY_TYPE_DATA} | ${'isLoadingTasksByTypeChart'} | ${true}
${types.RECEIVE_TASKS_BY_TYPE_DATA_ERROR} | ${'isLoadingTasksByTypeChart'} | ${false} ${types.RECEIVE_TASKS_BY_TYPE_DATA_ERROR} | ${'isLoadingTasksByTypeChart'} | ${false}
${types.REQUEST_UPDATE_STAGE} | ${'isLoading'} | ${true} ${types.REQUEST_UPDATE_STAGE} | ${'isLoading'} | ${true}
${types.RECEIVE_UPDATE_STAGE_RESPONSE} | ${'isLoading'} | ${false} ${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.REQUEST_REMOVE_STAGE} | ${'isLoading'} | ${true}
${types.RECEIVE_REMOVE_STAGE_RESPONSE} | ${'isLoading'} | ${false} ${types.RECEIVE_REMOVE_STAGE_RESPONSE} | ${'isLoading'} | ${false}
${types.REQUEST_DURATION_DATA} | ${'isLoadingDurationChart'} | ${true} ${types.REQUEST_DURATION_DATA} | ${'isLoadingDurationChart'} | ${true}
...@@ -116,6 +132,18 @@ describe('Cycle analytics mutations', () => { ...@@ -116,6 +132,18 @@ describe('Cycle analytics mutations', () => {
}); });
}); });
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);
expect(state.customStageFormErrors).toEqual(
convertObjectPropsToCamelCase(mockFormError.errors),
);
});
});
describe.each` describe.each`
mutation | value mutation | value
${types.REQUEST_GROUP_LABELS} | ${[]} ${types.REQUEST_GROUP_LABELS} | ${[]}
......
...@@ -502,6 +502,9 @@ msgstr "" ...@@ -502,6 +502,9 @@ msgstr ""
msgid "'%{level}' is not a valid visibility level" msgid "'%{level}' is not a valid visibility level"
msgstr "" msgstr ""
msgid "'%{name}' stage already exists"
msgstr ""
msgid "'%{source}' is not a import source" msgid "'%{source}' is not a import source"
msgstr "" msgstr ""
...@@ -19338,6 +19341,9 @@ msgstr "" ...@@ -19338,6 +19341,9 @@ msgstr ""
msgid "There was a problem communicating with your device." msgid "There was a problem communicating with your device."
msgstr "" msgstr ""
msgid "There was a problem refreshing the data, please try again"
msgstr ""
msgid "There was a problem saving your custom stage, please try again" msgid "There was a problem saving your custom stage, please try again"
msgstr "" msgstr ""
...@@ -22394,6 +22400,9 @@ msgstr "" ...@@ -22394,6 +22400,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again." msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr "" msgstr ""
msgid "Your custom stage '%{title}' was created"
msgstr ""
msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}." msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
msgstr "" msgstr ""
......
...@@ -123,7 +123,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do ...@@ -123,7 +123,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do
}) })
expect(stage).to be_invalid expect(stage).to be_invalid
expect(stage.errors[:start_event_label]).to include("can't be blank") expect(stage.errors[:start_event_label_id]).to include("can't be blank")
end end
it 'returns validation error when `end_event_label_id` is missing' do it 'returns validation error when `end_event_label_id` is missing' do
...@@ -135,7 +135,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do ...@@ -135,7 +135,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do
}) })
expect(stage).to be_invalid expect(stage).to be_invalid
expect(stage.errors[:end_event_label]).to include("can't be blank") expect(stage.errors[:end_event_label_id]).to include("can't be blank")
end end
end end
...@@ -145,7 +145,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do ...@@ -145,7 +145,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do
name: 'My Stage', name: 'My Stage',
parent: parent, parent: parent,
start_event_identifier: :issue_label_added, start_event_identifier: :issue_label_added,
start_event_label: group_label, start_event_label_id: group_label.id,
end_event_identifier: :issue_closed end_event_identifier: :issue_closed
}) })
...@@ -159,7 +159,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do ...@@ -159,7 +159,7 @@ RSpec.shared_examples 'cycle analytics label based stage' do
name: 'My Stage', name: 'My Stage',
parent: parent_in_subgroup, parent: parent_in_subgroup,
start_event_identifier: :issue_label_added, start_event_identifier: :issue_label_added,
start_event_label: group_label, start_event_label_id: group_label.id,
end_event_identifier: :issue_closed end_event_identifier: :issue_closed
}) })
...@@ -170,30 +170,30 @@ RSpec.shared_examples 'cycle analytics label based stage' do ...@@ -170,30 +170,30 @@ RSpec.shared_examples 'cycle analytics label based stage' do
context 'when label is defined for a different group' do context 'when label is defined for a different group' do
let(:error_message) { s_('CycleAnalyticsStage|is not available for the selected group') } let(:error_message) { s_('CycleAnalyticsStage|is not available for the selected group') }
it 'returns validation for `start_event_label`' do it 'returns validation for `start_event_label_id`' do
stage = described_class.new({ stage = described_class.new({
name: 'My Stage', name: 'My Stage',
parent: parent_outside_of_group_label_scope, parent: parent_outside_of_group_label_scope,
start_event_identifier: :issue_label_added, start_event_identifier: :issue_label_added,
start_event_label: group_label, start_event_label_id: group_label.id,
end_event_identifier: :issue_closed end_event_identifier: :issue_closed
}) })
expect(stage).to be_invalid expect(stage).to be_invalid
expect(stage.errors[:start_event_label]).to include(error_message) expect(stage.errors[:start_event_label_id]).to include(error_message)
end end
it 'returns validation for `end_event_label`' do it 'returns validation for `end_event_label_id`' do
stage = described_class.new({ stage = described_class.new({
name: 'My Stage', name: 'My Stage',
parent: parent_outside_of_group_label_scope, parent: parent_outside_of_group_label_scope,
start_event_identifier: :issue_closed, start_event_identifier: :issue_closed,
end_event_identifier: :issue_label_added, end_event_identifier: :issue_label_added,
end_event_label: group_label end_event_label_id: group_label.id
}) })
expect(stage).to be_invalid expect(stage).to be_invalid
expect(stage.errors[:end_event_label]).to include(error_message) expect(stage.errors[:end_event_label_id]).to include(error_message)
end end
end end
......
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