Commit a49b0d95 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Display form errors on relevant fields

Displays form errors below the relevant field
and will clear the error when a field changes

Fixed test for displaying form errors

Minor cleanup form errors after submission
and or when changing fields, or opening
a new form

Fix render warning start event change

Address minor reviewer feedback

Fixed string externalization

Fix minor typo

Updated gitlab.pot file with latest strings
parent f9584a7e
...@@ -68,6 +68,10 @@ export default { ...@@ -68,6 +68,10 @@ export default {
...defaultFields, ...defaultFields,
...this.initialFields, ...this.initialFields,
}, },
fieldErrors: {
...defaultFields,
...this.errors,
},
}; };
}, },
computed: { computed: {
...@@ -87,6 +91,11 @@ export default { ...@@ -87,6 +91,11 @@ export default {
hasStartEvent() { hasStartEvent() {
return this.fields.startEventIdentifier; return this.fields.startEventIdentifier;
}, },
endEventDescription() {
return !this.hasStartEvent
? s__('CustomCycleAnalytics|Please select a start event first')
: '';
},
startEventRequiresLabel() { startEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier); return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier);
}, },
...@@ -94,7 +103,7 @@ export default { ...@@ -94,7 +103,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 {
...@@ -121,20 +130,14 @@ export default { ...@@ -121,20 +130,14 @@ export default {
isDirty() { isDirty() {
return !isEqual(this.initialFields, this.fields) && !isEqual(defaultFields, 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
...@@ -153,6 +156,10 @@ export default { ...@@ -153,6 +156,10 @@ export default {
...defaultFields, ...defaultFields,
...newFields, ...newFields,
}; };
this.fieldErrors = {
...defaultFields,
...this.errors,
};
}, },
}, },
methods: { methods: {
...@@ -178,11 +185,23 @@ export default { ...@@ -178,11 +185,23 @@ export default {
handleClearLabel(key) { handleClearLabel(key) {
this.fields[key] = null; this.fields[key] = null;
}, },
isValid(key) { hasFieldErrors(key) {
return !this.isDirty || !this.errors || !this.errors[key]; return this.fieldErrors[key]?.length > 0;
},
fieldErrorMessage(key) {
return this.fieldErrors[key]?.join('\n');
}, },
fieldErrors(key) { onUpdateStartEventField() {
return !this.isValid(key) ? this.errors[key].join('\n') : null; 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);
}, },
}, },
}; };
...@@ -192,11 +211,12 @@ export default { ...@@ -192,11 +211,12 @@ export default {
<div class="mb-1"> <div class="mb-1">
<h4>{{ formTitle }}</h4> <h4>{{ formTitle }}</h4>
</div> </div>
<gl-form-group <gl-form-group
ref="name" ref="name"
:label="s__('CustomCycleAnalytics|Name')" :label="s__('CustomCycleAnalytics|Name')"
:state="isValid('name')" :state="!hasFieldErrors('name')"
:invalid-feedback="fieldErrors('name')" :invalid-feedback="fieldErrorMessage('name')"
> >
<gl-form-input <gl-form-input
v-model="fields.name" v-model="fields.name"
...@@ -206,31 +226,30 @@ export default { ...@@ -206,31 +226,30 @@ export default {
:placeholder="s__('CustomCycleAnalytics|Enter a name for the stage')" :placeholder="s__('CustomCycleAnalytics|Enter a name for the stage')"
required required
/> />
<!-- @change="onUpdateFormField" -->
</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 <gl-form-group
ref="startEventIdentifier" ref="startEventIdentifier"
:label="s__('CustomCycleAnalytics|Start event')" :label="s__('CustomCycleAnalytics|Start event')"
:state="isValid('startEventIdentifier')" :state="!hasFieldErrors('startEventIdentifier')"
:invalid-feedback="fieldErrors('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"
/> />
<!-- @change="onUpdateFormField" -->
</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 <gl-form-group
ref="startEventLabelId" ref="startEventLabelId"
:label="s__('CustomCycleAnalytics|Start event label')" :label="s__('CustomCycleAnalytics|Start event label')"
:state="isValid('startEventLabelId')" :state="!hasFieldErrors('startEventLabelId')"
:invalid-feedback="fieldErrors('startEventLabelId')" :invalid-feedback="fieldErrorMessage('startEventLabelId')"
> >
<labels-selector <labels-selector
:labels="labels" :labels="labels"
...@@ -239,7 +258,6 @@ export default { ...@@ -239,7 +258,6 @@ export default {
@selectLabel="handleSelectLabel('startEventLabelId', $event)" @selectLabel="handleSelectLabel('startEventLabelId', $event)"
@clearLabel="handleClearLabel('startEventLabelId')" @clearLabel="handleClearLabel('startEventLabelId')"
/> />
<!-- @change="onUpdateFormField" -->
</gl-form-group> </gl-form-group>
</div> </div>
</div> </div>
...@@ -248,11 +266,10 @@ export default { ...@@ -248,11 +266,10 @@ export default {
<gl-form-group <gl-form-group
ref="endEventIdentifier" ref="endEventIdentifier"
:label="s__('CustomCycleAnalytics|Stop event')" :label="s__('CustomCycleAnalytics|Stop event')"
:description=" :description="endEventDescription"
!hasStartEvent ? s__('CustomCycleAnalytics|Please select a start event first') : '' :state="!hasFieldErrors('endEventIdentifier')"
" :invalid-feedback="fieldErrorMessage('endEventIdentifier')"
:state="isValid('endEventIdentifier')" @change.native="onUpdateEndEventField"
:invalid-feedback="fieldErrors('endEventIdentifier') || endEventError"
> >
<gl-form-select <gl-form-select
v-model="fields.endEventIdentifier" v-model="fields.endEventIdentifier"
...@@ -261,15 +278,14 @@ export default { ...@@ -261,15 +278,14 @@ export default {
:required="true" :required="true"
:disabled="!hasStartEvent" :disabled="!hasStartEvent"
/> />
<!-- @change="onUpdateFormField" -->
</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 <gl-form-group
ref="endEventLabelId" ref="endEventLabelId"
:label="s__('CustomCycleAnalytics|Stop event label')" :label="s__('CustomCycleAnalytics|Stop event label')"
:state="isValid('endEventLabelId')" :state="!hasFieldErrors('endEventLabelId')"
:invalid-feedback="fieldErrors('endEventLabelId')" :invalid-feedback="fieldErrorMessage('endEventLabelId')"
> >
<labels-selector <labels-selector
:labels="labels" :labels="labels"
...@@ -278,7 +294,6 @@ export default { ...@@ -278,7 +294,6 @@ export default {
@selectLabel="handleSelectLabel('endEventLabelId', $event)" @selectLabel="handleSelectLabel('endEventLabelId', $event)"
@clearLabel="handleClearLabel('endEventLabelId')" @clearLabel="handleClearLabel('endEventLabelId')"
/> />
<!-- @change="onUpdateFormField" -->
</gl-form-group> </gl-form-group>
</div> </div>
</div> </div>
......
<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';
...@@ -91,6 +92,7 @@ export default { ...@@ -91,6 +92,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['customStageFormInitialData']),
stageEventsHeight() { stageEventsHeight() {
return `${this.stageNavHeight}px`; return `${this.stageNavHeight}px`;
}, },
...@@ -132,27 +134,6 @@ export default { ...@@ -132,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);
...@@ -212,7 +193,7 @@ export default { ...@@ -212,7 +193,7 @@ 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" :errors="customStageFormErrors"
@submit="$emit('submit', $event)" @submit="$emit('submit', $event)"
......
...@@ -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 && errors.name[0] === 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 showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {}) => { export const showEditCustomStageForm = ({ commit, dispatch }, selectedStage = {}) => {
commit(types.SHOW_EDIT_CUSTOM_STAGE_FORM); 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,13 +268,15 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => { ...@@ -236,13 +268,15 @@ export const fetchGroupStagesAndEvents = ({ state, dispatch, getters }) => {
); );
}; };
export const clearCustomStageFormErrors = ({ commit }) => export const clearCustomStageFormErrors = ({ commit }) => {
commit(types.CLEAR_CUSTOM_STAGE_FORM_ERRORS); 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_SUCCESS); 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 dispatch('fetchGroupStagesAndEvents').then(() => dispatch('fetchSummaryData'));
}; };
...@@ -251,10 +285,9 @@ export const receiveCreateCustomStageError = ({ commit }, { status, message, err ...@@ -251,10 +285,9 @@ export const receiveCreateCustomStageError = ({ commit }, { status, message, err
commit(types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR, { status, message, errors, data }); commit(types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR, { status, message, errors, data });
const { name } = data; const { name } = data;
const flashMessage = const flashMessage = 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(flashMessage); createFlash(flashMessage);
}; };
...@@ -329,19 +362,15 @@ export const receiveUpdateStageSuccess = ({ commit, dispatch }, updatedData) => ...@@ -329,19 +362,15 @@ export const receiveUpdateStageSuccess = ({ commit, dispatch }, updatedData) =>
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_ERROR); 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));
}; };
...@@ -355,7 +384,9 @@ export const updateStage = ({ dispatch, state }, { id, ...rest }) => { ...@@ -355,7 +384,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);
......
...@@ -97,14 +97,20 @@ export default { ...@@ -97,14 +97,20 @@ export default {
[types.SHOW_CUSTOM_STAGE_FORM](state) { [types.SHOW_CUSTOM_STAGE_FORM](state) {
state.isCreatingCustomStage = true; state.isCreatingCustomStage = true;
state.isEditingCustomStage = false; state.isEditingCustomStage = false;
state.customStageFormInitialData = null;
state.customStageFormErrors = null;
}, },
[types.SHOW_EDIT_CUSTOM_STAGE_FORM](state) { [types.SHOW_EDIT_CUSTOM_STAGE_FORM](state, initialData) {
state.isEditingCustomStage = true; state.isEditingCustomStage = true;
state.isCreatingCustomStage = false; 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.customStageFormInitialData = null;
state.customStageFormErrors = null;
}, },
[types.CLEAR_CUSTOM_STAGE_FORM_ERRORS](state) { [types.CLEAR_CUSTOM_STAGE_FORM_ERRORS](state) {
state.customStageFormErrors = null; state.customStageFormErrors = null;
...@@ -160,23 +166,26 @@ export default { ...@@ -160,23 +166,26 @@ export default {
}, },
[types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS](state) { [types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS](state) {
state.isSavingCustomStage = false; state.isSavingCustomStage = false;
state.customStageFormErrors = {}; 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.isSavingCustomStage = true;
state.customStageFormErrors = {}; state.customStageFormErrors = null;
}, },
[types.RECEIVE_UPDATE_STAGE_SUCCESS](state) { [types.RECEIVE_UPDATE_STAGE_SUCCESS](state) {
state.isLoading = false; state.isLoading = false;
state.isSavingCustomStage = false; state.isSavingCustomStage = false;
state.isEditingCustomStage = false; state.isEditingCustomStage = false;
state.customStageFormErrors = {}; state.customStageFormErrors = null;
state.customStageFormInitialData = null;
}, },
[types.RECEIVE_UPDATE_STAGE_ERROR](state, { errors = null } = {}) { [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.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,7 +31,8 @@ export default () => ({ ...@@ -31,7 +31,8 @@ export default () => ({
medians: {}, medians: {},
customStageFormEvents: [], customStageFormEvents: [],
customStageFormErrors: {}, customStageFormErrors: null,
customStageFormInitialData: null,
tasksByType: { tasksByType: {
subject: TASKS_BY_TYPE_SUBJECT_ISSUE, subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
......
...@@ -322,7 +322,7 @@ describe 'Group Value Stream Analytics', :js do ...@@ -322,7 +322,7 @@ describe 'Group Value Stream Analytics', :js do
def select_dropdown_label(field, index = 2) def select_dropdown_label(field, index = 2)
page.find("[name=#{field}] .dropdown-toggle").click page.find("[name=#{field}] .dropdown-toggle").click
page.find("[name=#{field}] .dropdown-menu").all('.dropdown-item')[2].click page.find("[name=#{field}] .dropdown-menu").all('.dropdown-item')[index].click
end end
context 'enabled' do context 'enabled' do
...@@ -380,12 +380,6 @@ describe 'Group Value Stream Analytics', :js do ...@@ -380,12 +380,6 @@ describe 'Group Value Stream Analytics', :js do
expect(page).to have_button('Add stage', disabled: true) expect(page).to have_button('Add stage', disabled: true)
end end
it 'an error message is displayed if the start event is changed' do
select_dropdown_option 'custom-stage-start-event', 'option', 2
expect(page).to have_text 'Start event changed, please select a valid stop event'
end
it 'the custom stage is saved' do it 'the custom stage is saved' do
click_button 'Add stage' click_button 'Add stage'
......
...@@ -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 Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = ` exports[`CustomStageForm Empty form 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 Start event with events does not select events with can ...@@ -29,7 +29,7 @@ exports[`CustomStageForm Start event with events does not select events with can
</select>" </select>"
`; `;
exports[`CustomStageForm Start event with events selects events with canBeStartEvent=true for the start events dropdown 1`] = ` exports[`CustomStageForm Empty form 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,46 +52,56 @@ exports[`CustomStageForm Start event with events selects events with canBeStartE ...@@ -52,46 +52,56 @@ exports[`CustomStageForm Start event with events selects events with canBeStartE
</select>" </select>"
`; `;
exports[`CustomStageForm With errors renders the errors for the relevant fields 1`] = ` exports[`CustomStageForm Empty form isSavingCustomStage=true displays a loading icon 1`] = `
"<fieldset aria-invalid=\\"true\\" class=\\"form-group gl-form-group is-invalid\\" id=\\"__BVID__1372\\" aria-describedby=\\"__BVID__1372__BV_feedback_invalid_\\"> "<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>
<legend tabindex=\\"-1\\" class=\\"col-form-label pt-0 col-form-label\\" id=\\"__BVID__1372__BV_label_\\">Name</legend> Add stage
<div tabindex=\\"-1\\" role=\\"group\\"><input name=\\"custom-stage-name\\" type=\\"text\\" placeholder=\\"Enter a name for the stage\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-input form-control form-control\\" id=\\"__BVID__1374\\"> </button>"
<div tabindex=\\"-1\\" role=\\"alert\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\" class=\\"invalid-feedback d-block\\" id=\\"__BVID__1372__BV_feedback_invalid_\\">is reserved
cant be blank</div>
<!---->
<!---->
</div>
</fieldset>"
`; `;
exports[`CustomStageForm With errors renders the errors for the relevant fields 2`] = ` exports[`CustomStageForm Start event with events does not select events with canBeStartEvent=false for the start events dropdown 1`] = `
"<fieldset aria-invalid=\\"true\\" class=\\"form-group gl-form-group is-invalid\\" id=\\"__BVID__1376\\" aria-describedby=\\"__BVID__1376__BV_feedback_invalid_\\"> "<select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__123\\">
<legend tabindex=\\"-1\\" class=\\"col-form-label pt-0 col-form-label\\" id=\\"__BVID__1376__BV_label_\\">Start event</legend> <option value=\\"\\">Select start event</option>
<div tabindex=\\"-1\\" role=\\"group\\"><select name=\\"custom-stage-start-event\\" required=\\"required\\" aria-required=\\"true\\" class=\\"gl-form-select custom-select\\" id=\\"__BVID__1378\\"> <option value=\\"issue_created\\">Issue created</option>
<option value=\\"\\">Select start event</option> <option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
<option value=\\"issue_created\\">Issue created</option> <option value=\\"merge_request_created\\">Merge request created</option>
<option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option> <option value=\\"merge_request_first_deployed_to_production\\">Merge request first deployed to production</option>
<option value=\\"merge_request_created\\">Merge request created</option> <option value=\\"merge_request_last_build_finished\\">Merge request last build finish time</option>
<option value=\\"merge_request_first_deployed_to_production\\">Merge request first deployed to production</option> <option value=\\"merge_request_last_build_started\\">Merge request last build start time</option>
<option value=\\"merge_request_last_build_finished\\">Merge request last build finish time</option> <option value=\\"merge_request_merged\\">Merge request merged</option>
<option value=\\"merge_request_last_build_started\\">Merge request last build start time</option> <option value=\\"code_stage_start\\">Issue first mentioned in a commit</option>
<option value=\\"merge_request_merged\\">Merge request merged</option> <option value=\\"plan_stage_start\\">Issue first associated with a milestone or issue first added to a board</option>
<option value=\\"code_stage_start\\">Issue first mentioned in a commit</option> <option value=\\"issue_closed\\">Issue closed</option>
<option value=\\"plan_stage_start\\">Issue first associated with a milestone or issue first added to a board</option> <option value=\\"issue_first_added_to_board\\">Issue first added to a board</option>
<option value=\\"issue_closed\\">Issue closed</option> <option value=\\"issue_first_associated_with_milestone\\">Issue first associated with a milestone</option>
<option value=\\"issue_first_added_to_board\\">Issue first added to a board</option> <option value=\\"issue_label_added\\">Issue label was added</option>
<option value=\\"issue_first_associated_with_milestone\\">Issue first associated with a milestone</option> <option value=\\"issue_label_removed\\">Issue label was removed</option>
<option value=\\"issue_label_added\\">Issue label was added</option> <option value=\\"merge_request_closed\\">Merge request closed</option>
<option value=\\"issue_label_removed\\">Issue label was removed</option> <option value=\\"merge_request_label_added\\">Merge Request label was added</option>
<option value=\\"merge_request_closed\\">Merge request closed</option> <option value=\\"merge_request_label_removed\\">Merge Request label was removed</option>
<option value=\\"merge_request_label_added\\">Merge Request label was added</option> </select>"
<option value=\\"merge_request_label_removed\\">Merge Request label was removed</option> `;
</select>
<div tabindex=\\"-1\\" role=\\"alert\\" aria-live=\\"assertive\\" aria-atomic=\\"true\\" class=\\"invalid-feedback d-block\\" id=\\"__BVID__1376__BV_feedback_invalid_\\">cant be blank</div> 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\\">
<!----> <option value=\\"\\">Select start event</option>
</div> <option value=\\"issue_created\\">Issue created</option>
</fieldset>" <option value=\\"issue_first_mentioned_in_commit\\">Issue first mentioned in a commit</option>
<option value=\\"merge_request_created\\">Merge request created</option>
<option value=\\"merge_request_first_deployed_to_production\\">Merge request first deployed to production</option>
<option value=\\"merge_request_last_build_finished\\">Merge request last build finish time</option>
<option value=\\"merge_request_last_build_started\\">Merge request last build start time</option>
<option value=\\"merge_request_merged\\">Merge request merged</option>
<option value=\\"code_stage_start\\">Issue first mentioned in a commit</option>
<option value=\\"plan_stage_start\\">Issue first associated with a milestone or issue first added to a board</option>
<option value=\\"issue_closed\\">Issue closed</option>
<option value=\\"issue_first_added_to_board\\">Issue first added to a board</option>
<option value=\\"issue_first_associated_with_milestone\\">Issue first associated with a milestone</option>
<option value=\\"issue_label_added\\">Issue label was added</option>
<option value=\\"issue_label_removed\\">Issue label was removed</option>
<option value=\\"merge_request_closed\\">Merge request closed</option>
<option value=\\"merge_request_label_added\\">Merge Request label was added</option>
<option value=\\"merge_request_label_removed\\">Merge Request label was removed</option>
</select>"
`; `;
exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = ` exports[`CustomStageForm isSavingCustomStage=true displays a loading icon 1`] = `
......
...@@ -668,6 +668,7 @@ describe('CustomStageForm', () => { ...@@ -668,6 +668,7 @@ describe('CustomStageForm', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ wrapper = createComponent({
initialFields: initData, initialFields: initData,
errors: customStageFormErrors,
}); });
return Vue.nextTick(); return Vue.nextTick();
...@@ -678,14 +679,9 @@ describe('CustomStageForm', () => { ...@@ -678,14 +679,9 @@ describe('CustomStageForm', () => {
}); });
it('renders the errors for the relevant fields', () => { it('renders the errors for the relevant fields', () => {
console.log('wrapper', wrapper.html()); expect(wrapper.find({ ref: 'name' }).html()).toContain('is reserved');
expect(wrapper.find({ ref: 'name' }).html()).toContain('cant be blank');
// const errorMessages = wrapper.findAll(sel.invalidFeedback); expect(wrapper.find({ ref: 'startEventIdentifier' }).html()).toContain('cant be blank');
// expect(errorMessages.length).toEqual(2);
wrapper.setProps({ errors: customStageFormErrors });
expect(wrapper.find({ ref: 'startEventIdentifier' }).html()).toContain('CAnt be beelebel');
expect(wrapper.find({ ref: 'name' }).html()).toContain('CAnt be beelebel');
}); });
}); });
}); });
...@@ -626,6 +626,7 @@ describe('Cycle analytics actions', () => { ...@@ -626,6 +626,7 @@ describe('Cycle analytics actions', () => {
it('dispatches receiveUpdateStageError', done => { it('dispatches receiveUpdateStageError', done => {
const data = { const data = {
id: stageId, id: stageId,
name: 'issue',
...payload, ...payload,
}; };
testAction( testAction(
...@@ -637,7 +638,10 @@ describe('Cycle analytics actions', () => { ...@@ -637,7 +638,10 @@ describe('Cycle analytics actions', () => {
{ type: 'requestUpdateStage' }, { type: 'requestUpdateStage' },
{ {
type: 'receiveUpdateStageError', type: 'receiveUpdateStageError',
payload: { error, data }, payload: {
status: 404,
data,
},
}, },
], ],
done, done,
...@@ -651,13 +655,9 @@ describe('Cycle analytics actions', () => { ...@@ -651,13 +655,9 @@ describe('Cycle analytics actions', () => {
state, state,
}, },
{ {
error: { status: 422,
response: { responseData: {
status: 422, errors: { name: ['is reserved'] },
data: {
errors: { name: ['is reserved'] },
},
},
}, },
data: { data: {
name: stageId, name: stageId,
...@@ -675,7 +675,7 @@ describe('Cycle analytics actions', () => { ...@@ -675,7 +675,7 @@ describe('Cycle analytics actions', () => {
commit: () => {}, commit: () => {},
state, state,
}, },
{}, { status: 400 },
); );
shouldFlashAMessage('There was a problem saving your custom stage, please try again'); shouldFlashAMessage('There was a problem saving your custom stage, please try again');
......
...@@ -37,10 +37,15 @@ describe('Cycle analytics mutations', () => { ...@@ -37,10 +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.SHOW_CUSTOM_STAGE_FORM} | ${'isEditingCustomStage'} | ${false} ${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} | ${'isEditingCustomStage'} | ${true}
${types.SHOW_EDIT_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false} ${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}
...@@ -61,11 +66,11 @@ describe('Cycle analytics mutations', () => { ...@@ -61,11 +66,11 @@ describe('Cycle analytics mutations', () => {
${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.REQUEST_UPDATE_STAGE} | ${'isSavingCustomStage'} | ${true} ${types.REQUEST_UPDATE_STAGE} | ${'isSavingCustomStage'} | ${true}
${types.REQUEST_UPDATE_STAGE} | ${'customStageFormErrors'} | ${{}} ${types.REQUEST_UPDATE_STAGE} | ${'customStageFormErrors'} | ${null}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isLoading'} | ${false} ${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isLoading'} | ${false}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isSavingCustomStage'} | ${false} ${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isSavingCustomStage'} | ${false}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isEditingCustomStage'} | ${false} ${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'isEditingCustomStage'} | ${false}
${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'customStageFormErrors'} | ${{}} ${types.RECEIVE_UPDATE_STAGE_SUCCESS} | ${'customStageFormErrors'} | ${null}
${types.RECEIVE_UPDATE_STAGE_ERROR} | ${'isLoading'} | ${false} ${types.RECEIVE_UPDATE_STAGE_ERROR} | ${'isLoading'} | ${false}
${types.RECEIVE_UPDATE_STAGE_ERROR} | ${'isSavingCustomStage'} | ${false} ${types.RECEIVE_UPDATE_STAGE_ERROR} | ${'isSavingCustomStage'} | ${false}
${types.REQUEST_REMOVE_STAGE} | ${'isLoading'} | ${true} ${types.REQUEST_REMOVE_STAGE} | ${'isLoading'} | ${true}
......
...@@ -496,6 +496,9 @@ msgstr "" ...@@ -496,6 +496,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 ""
...@@ -22379,6 +22382,9 @@ msgstr "" ...@@ -22379,6 +22382,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 ""
......
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