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 {
...defaultFields,
...this.initialFields,
},
fieldErrors: {
...defaultFields,
...this.errors,
},
};
},
computed: {
......@@ -87,6 +91,11 @@ export default {
hasStartEvent() {
return this.fields.startEventIdentifier;
},
endEventDescription() {
return !this.hasStartEvent
? s__('CustomCycleAnalytics|Please select a start event first')
: '';
},
startEventRequiresLabel() {
return isLabelEvent(this.labelEvents, this.fields.startEventIdentifier);
},
......@@ -94,7 +103,7 @@ export default {
return isLabelEvent(this.labelEvents, this.fields.endEventIdentifier);
},
isComplete() {
if (!this.hasValidStartAndEndEventPair) {
if (this.eventMismatchError) {
return false;
}
const {
......@@ -121,20 +130,14 @@ export default {
isDirty() {
return !isEqual(this.initialFields, this.fields) && !isEqual(defaultFields, this.fields);
},
hasValidStartAndEndEventPair() {
eventMismatchError() {
const {
fields: { startEventIdentifier, endEventIdentifier },
fields: { startEventIdentifier = null, endEventIdentifier = null },
} = this;
if (startEventIdentifier && 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;
if (!startEventIdentifier || !endEventIdentifier) return true;
const endEvents = getAllowedEndEvents(this.events, startEventIdentifier);
return !endEvents.length || !endEvents.includes(endEventIdentifier);
},
saveStageText() {
return this.isEditingCustomStage
......@@ -153,6 +156,10 @@ export default {
...defaultFields,
...newFields,
};
this.fieldErrors = {
...defaultFields,
...this.errors,
};
},
},
methods: {
......@@ -178,11 +185,23 @@ export default {
handleClearLabel(key) {
this.fields[key] = null;
},
isValid(key) {
return !this.isDirty || !this.errors || !this.errors[key];
hasFieldErrors(key) {
return this.fieldErrors[key]?.length > 0;
},
fieldErrorMessage(key) {
return this.fieldErrors[key]?.join('\n');
},
fieldErrors(key) {
return !this.isValid(key) ? this.errors[key].join('\n') : null;
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);
},
},
};
......@@ -192,11 +211,12 @@ export default {
<div class="mb-1">
<h4>{{ formTitle }}</h4>
</div>
<gl-form-group
ref="name"
:label="s__('CustomCycleAnalytics|Name')"
:state="isValid('name')"
:invalid-feedback="fieldErrors('name')"
:state="!hasFieldErrors('name')"
:invalid-feedback="fieldErrorMessage('name')"
>
<gl-form-input
v-model="fields.name"
......@@ -206,31 +226,30 @@ export default {
:placeholder="s__('CustomCycleAnalytics|Enter a name for the stage')"
required
/>
<!-- @change="onUpdateFormField" -->
</gl-form-group>
<div class="d-flex" :class="{ 'justify-content-between': startEventRequiresLabel }">
<div :class="[startEventRequiresLabel ? 'w-50 mr-1' : 'w-100']">
<gl-form-group
ref="startEventIdentifier"
:label="s__('CustomCycleAnalytics|Start event')"
:state="isValid('startEventIdentifier')"
:invalid-feedback="fieldErrors('startEventIdentifier')"
:state="!hasFieldErrors('startEventIdentifier')"
:invalid-feedback="fieldErrorMessage('startEventIdentifier')"
>
<gl-form-select
v-model="fields.startEventIdentifier"
name="custom-stage-start-event"
:required="true"
:options="startEventOptions"
@change.native="onUpdateStartEventField"
/>
<!-- @change="onUpdateFormField" -->
</gl-form-group>
</div>
<div v-if="startEventRequiresLabel" class="w-50 ml-1">
<gl-form-group
ref="startEventLabelId"
:label="s__('CustomCycleAnalytics|Start event label')"
:state="isValid('startEventLabelId')"
:invalid-feedback="fieldErrors('startEventLabelId')"
:state="!hasFieldErrors('startEventLabelId')"
:invalid-feedback="fieldErrorMessage('startEventLabelId')"
>
<labels-selector
:labels="labels"
......@@ -239,7 +258,6 @@ export default {
@selectLabel="handleSelectLabel('startEventLabelId', $event)"
@clearLabel="handleClearLabel('startEventLabelId')"
/>
<!-- @change="onUpdateFormField" -->
</gl-form-group>
</div>
</div>
......@@ -248,11 +266,10 @@ export default {
<gl-form-group
ref="endEventIdentifier"
:label="s__('CustomCycleAnalytics|Stop event')"
:description="
!hasStartEvent ? s__('CustomCycleAnalytics|Please select a start event first') : ''
"
:state="isValid('endEventIdentifier')"
:invalid-feedback="fieldErrors('endEventIdentifier') || endEventError"
:description="endEventDescription"
:state="!hasFieldErrors('endEventIdentifier')"
:invalid-feedback="fieldErrorMessage('endEventIdentifier')"
@change.native="onUpdateEndEventField"
>
<gl-form-select
v-model="fields.endEventIdentifier"
......@@ -261,15 +278,14 @@ export default {
:required="true"
:disabled="!hasStartEvent"
/>
<!-- @change="onUpdateFormField" -->
</gl-form-group>
</div>
<div v-if="endEventRequiresLabel" class="w-50 ml-1">
<gl-form-group
ref="endEventLabelId"
:label="s__('CustomCycleAnalytics|Stop event label')"
:state="isValid('endEventLabelId')"
:invalid-feedback="fieldErrors('endEventLabelId')"
:state="!hasFieldErrors('endEventLabelId')"
:invalid-feedback="fieldErrorMessage('endEventLabelId')"
>
<labels-selector
:labels="labels"
......@@ -278,7 +294,6 @@ export default {
@selectLabel="handleSelectLabel('endEventLabelId', $event)"
@clearLabel="handleClearLabel('endEventLabelId')"
/>
<!-- @change="onUpdateFormField" -->
</gl-form-group>
</div>
</div>
......
<script>
import { mapState } from 'vuex';
import { GlTooltipDirective, GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import StageNavItem from './stage_nav_item.vue';
......@@ -91,6 +92,7 @@ export default {
};
},
computed: {
...mapState(['customStageFormInitialData']),
stageEventsHeight() {
return `${this.stageNavHeight}px`;
},
......@@ -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() {
this.$set(this, 'stageNavHeight', this.$refs.stageNav.clientHeight);
......@@ -212,7 +193,7 @@ export default {
:events="customStageFormEvents"
:labels="labels"
:is-saving-custom-stage="isSavingCustomStage"
:initial-fields="customStageInitialData"
:initial-fields="customStageFormInitialData"
:is-editing-custom-stage="isEditingCustomStage"
:errors="customStageFormErrors"
@submit="$emit('submit', $event)"
......
......@@ -2,7 +2,7 @@ import dateFormat from 'dateformat';
import Api from 'ee/api';
import { getDayDifference, getDateInPast } from '~/lib/utils/datetime_utility';
import createFlash, { hideFlash } from '~/flash';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import httpStatus from '~/lib/utils/http_status';
import * as types from './mutation_types';
import { dateFormats } from '../../shared/constants';
......@@ -21,6 +21,14 @@ const handleErrorOrRethrow = ({ action, error }) => {
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) =>
commit(types.SET_FEATURE_FLAGS, featureFlags);
export const setSelectedGroup = ({ commit }, group) => commit(types.SET_SELECTED_GROUP, group);
......@@ -135,12 +143,36 @@ export const fetchCycleAnalyticsData = ({ dispatch }) => {
.catch(error => dispatch('receiveCycleAnalyticsDataError', error));
};
export const hideCustomStageForm = ({ commit }) => commit(types.HIDE_CUSTOM_STAGE_FORM);
export const showCustomStageForm = ({ commit }) => commit(types.SHOW_CUSTOM_STAGE_FORM);
export const hideCustomStageForm = ({ commit }) => {
commit(types.HIDE_CUSTOM_STAGE_FORM);
removeError();
};
export const showCustomStageForm = ({ commit }) => {
commit(types.SHOW_CUSTOM_STAGE_FORM);
removeError();
};
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);
removeError();
};
export const requestSummaryData = ({ commit }) => commit(types.REQUEST_SUMMARY_DATA);
......@@ -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);
removeError();
};
export const requestCreateCustomStage = ({ commit }) => commit(types.REQUEST_CREATE_CUSTOM_STAGE);
export const receiveCreateCustomStageSuccess = ({ commit, dispatch }, { data: { title } }) => {
commit(types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS);
createFlash(__(`Your custom stage '${title}' was created`), 'notice');
createFlash(sprintf(__(`Your custom stage '%{title}' was created`), { title }), 'notice');
return dispatch('fetchGroupStagesAndEvents').then(() => dispatch('fetchSummaryData'));
};
......@@ -251,10 +285,9 @@ export const receiveCreateCustomStageError = ({ commit }, { status, message, err
commit(types.RECEIVE_CREATE_CUSTOM_STAGE_ERROR, { status, message, errors, data });
const { name } = data;
const flashMessage =
status !== httpStatus.UNPROCESSABLE_ENTITY
? __(`'${name}' stage already exists'`)
: __('There was a problem saving your custom stage, please try again');
const flashMessage = isStageNameExistsError({ status, errors })
? sprintf(__(`'%{name}' stage already exists`), { name })
: __('There was a problem saving your custom stage, please try again');
createFlash(flashMessage);
};
......@@ -329,19 +362,15 @@ export const receiveUpdateStageSuccess = ({ commit, dispatch }, updatedData) =>
export const receiveUpdateStageError = (
{ commit },
{ error: { response: { status = 400, data: errorData } = {} } = {}, data },
{ status, responseData: { errors = null } = {}, data = {} },
) => {
commit(types.RECEIVE_UPDATE_STAGE_ERROR);
const ERROR_NAME_RESERVED = 'is reserved';
commit(types.RECEIVE_UPDATE_STAGE_ERROR, { errors, data });
let message = __('There was a problem saving your custom stage, please try again');
if (status && status === httpStatus.UNPROCESSABLE_ENTITY) {
const { errors } = errorData;
const { name } = data;
if (errors.name && errors.name[0] === ERROR_NAME_RESERVED) {
message = __(`'${name}' stage already exists`);
}
}
const { name = null } = data;
const message =
name && isStageNameExistsError({ status, errors })
? sprintf(__(`'%{name}' stage already exists`), { name })
: __('There was a problem saving your custom stage, please try again');
createFlash(__(message));
};
......@@ -355,7 +384,9 @@ export const updateStage = ({ dispatch, state }, { id, ...rest }) => {
return Api.cycleAnalyticsUpdateStage(id, fullPath, { ...rest })
.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);
......
......@@ -97,14 +97,20 @@ export default {
[types.SHOW_CUSTOM_STAGE_FORM](state) {
state.isCreatingCustomStage = true;
state.isEditingCustomStage = false;
state.customStageFormInitialData = null;
state.customStageFormErrors = null;
},
[types.SHOW_EDIT_CUSTOM_STAGE_FORM](state) {
[types.SHOW_EDIT_CUSTOM_STAGE_FORM](state, initialData) {
state.isEditingCustomStage = true;
state.isCreatingCustomStage = false;
state.customStageFormInitialData = initialData;
state.customStageFormErrors = null;
},
[types.HIDE_CUSTOM_STAGE_FORM](state) {
state.isEditingCustomStage = false;
state.isCreatingCustomStage = false;
state.customStageFormInitialData = null;
state.customStageFormErrors = null;
},
[types.CLEAR_CUSTOM_STAGE_FORM_ERRORS](state) {
state.customStageFormErrors = null;
......@@ -160,23 +166,26 @@ export default {
},
[types.RECEIVE_CREATE_CUSTOM_STAGE_SUCCESS](state) {
state.isSavingCustomStage = false;
state.customStageFormErrors = {};
state.customStageFormErrors = null;
state.customStageFormInitialData = null;
},
[types.REQUEST_UPDATE_STAGE](state) {
state.isLoading = true;
state.isSavingCustomStage = true;
state.customStageFormErrors = {};
state.customStageFormErrors = null;
},
[types.RECEIVE_UPDATE_STAGE_SUCCESS](state) {
state.isLoading = false;
state.isSavingCustomStage = 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.isSavingCustomStage = false;
state.customStageFormErrors = convertObjectPropsToCamelCase(errors, { deep: true });
state.customStageFormInitialData = convertObjectPropsToCamelCase(data, { deep: true });
},
[types.REQUEST_REMOVE_STAGE](state) {
state.isLoading = true;
......
......@@ -31,7 +31,8 @@ export default () => ({
medians: {},
customStageFormEvents: [],
customStageFormErrors: {},
customStageFormErrors: null,
customStageFormInitialData: null,
tasksByType: {
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
......
......@@ -322,7 +322,7 @@ describe 'Group Value Stream Analytics', :js do
def select_dropdown_label(field, index = 2)
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
context 'enabled' do
......@@ -380,12 +380,6 @@ describe 'Group Value Stream Analytics', :js do
expect(page).to have_button('Add stage', disabled: true)
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
click_button 'Add stage'
......
......@@ -6,7 +6,7 @@ exports[`CustomStageForm Editing a custom stage isSavingCustomStage=true display
</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\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option>
......@@ -29,7 +29,7 @@ exports[`CustomStageForm Start event with events does not select events with can
</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\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option>
......@@ -52,46 +52,56 @@ exports[`CustomStageForm Start event with events selects events with canBeStartE
</select>"
`;
exports[`CustomStageForm With errors renders the errors for the relevant fields 1`] = `
"<fieldset aria-invalid=\\"true\\" class=\\"form-group gl-form-group is-invalid\\" id=\\"__BVID__1372\\" aria-describedby=\\"__BVID__1372__BV_feedback_invalid_\\">
<legend tabindex=\\"-1\\" class=\\"col-form-label pt-0 col-form-label\\" id=\\"__BVID__1372__BV_label_\\">Name</legend>
<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\\">
<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 Empty form 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>
Add stage
</button>"
`;
exports[`CustomStageForm With errors renders the errors for the relevant fields 2`] = `
"<fieldset aria-invalid=\\"true\\" class=\\"form-group gl-form-group is-invalid\\" id=\\"__BVID__1376\\" aria-describedby=\\"__BVID__1376__BV_feedback_invalid_\\">
<legend tabindex=\\"-1\\" class=\\"col-form-label pt-0 col-form-label\\" id=\\"__BVID__1376__BV_label_\\">Start event</legend>
<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=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option>
<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>
<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>
<!---->
<!---->
</div>
</fieldset>"
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\\">
<option value=\\"\\">Select start event</option>
<option value=\\"issue_created\\">Issue created</option>
<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 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>
<option value=\\"issue_created\\">Issue created</option>
<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`] = `
......
......@@ -668,6 +668,7 @@ describe('CustomStageForm', () => {
beforeEach(() => {
wrapper = createComponent({
initialFields: initData,
errors: customStageFormErrors,
});
return Vue.nextTick();
......@@ -678,14 +679,9 @@ describe('CustomStageForm', () => {
});
it('renders the errors for the relevant fields', () => {
console.log('wrapper', wrapper.html());
// const errorMessages = wrapper.findAll(sel.invalidFeedback);
// 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');
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');
});
});
});
......@@ -626,6 +626,7 @@ describe('Cycle analytics actions', () => {
it('dispatches receiveUpdateStageError', done => {
const data = {
id: stageId,
name: 'issue',
...payload,
};
testAction(
......@@ -637,7 +638,10 @@ describe('Cycle analytics actions', () => {
{ type: 'requestUpdateStage' },
{
type: 'receiveUpdateStageError',
payload: { error, data },
payload: {
status: 404,
data,
},
},
],
done,
......@@ -651,13 +655,9 @@ describe('Cycle analytics actions', () => {
state,
},
{
error: {
response: {
status: 422,
data: {
errors: { name: ['is reserved'] },
},
},
status: 422,
responseData: {
errors: { name: ['is reserved'] },
},
data: {
name: stageId,
......@@ -675,7 +675,7 @@ describe('Cycle analytics actions', () => {
commit: () => {},
state,
},
{},
{ status: 400 },
);
shouldFlashAMessage('There was a problem saving your custom stage, please try again');
......
......@@ -37,10 +37,15 @@ describe('Cycle analytics mutations', () => {
it.each`
mutation | stateKey | value
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'isEditingCustomStage'} | ${false}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'customStageFormErrors'} | ${null}
${types.HIDE_CUSTOM_STAGE_FORM} | ${'customStageFormInitialData'} | ${null}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${true}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'isEditingCustomStage'} | ${false}
${types.SHOW_CUSTOM_STAGE_FORM} | ${'customStageFormErrors'} | ${null}
${types.SHOW_EDIT_CUSTOM_STAGE_FORM} | ${'isEditingCustomStage'} | ${true}
${types.SHOW_EDIT_CUSTOM_STAGE_FORM} | ${'isCreatingCustomStage'} | ${false}
${types.SHOW_EDIT_CUSTOM_STAGE_FORM} | ${'customStageFormErrors'} | ${null}
${types.REQUEST_STAGE_DATA} | ${'isLoadingStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isEmptyStage'} | ${true}
${types.RECEIVE_STAGE_DATA_ERROR} | ${'isLoadingStage'} | ${false}
......@@ -61,11 +66,11 @@ describe('Cycle analytics mutations', () => {
${types.RECEIVE_TASKS_BY_TYPE_DATA_ERROR} | ${'isLoadingTasksByTypeChart'} | ${false}
${types.REQUEST_UPDATE_STAGE} | ${'isLoading'} | ${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} | ${'isSavingCustomStage'} | ${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} | ${'isSavingCustomStage'} | ${false}
${types.REQUEST_REMOVE_STAGE} | ${'isLoading'} | ${true}
......
......@@ -496,6 +496,9 @@ msgstr ""
msgid "'%{level}' is not a valid visibility level"
msgstr ""
msgid "'%{name}' stage already exists"
msgstr ""
msgid "'%{source}' is not a import source"
msgstr ""
......@@ -22379,6 +22382,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
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}."
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