Commit 3bbb9a7b authored by Michael Kozono's avatar Michael Kozono

Merge branch '35546-child-epic-error-messages' into 'master'

Return meaningful error when adding one child epic at time and it fails

See merge request gitlab-org/gitlab!22688
parents e99879ba 942a6152
......@@ -19,6 +19,7 @@ const httpStatusCodes = {
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
GONE: 410,
UNPROCESSABLE_ENTITY: 422,
SERVICE_UNAVAILABLE: 503,
......
---
title: Improve error messages when adding a child epic
merge_request: 22688
author:
type: fixed
......@@ -59,6 +59,11 @@ export default {
required: false,
default: itemAddFailureTypesMap.NOT_FOUND,
},
itemAddFailureMessage: {
type: String,
required: false,
default: '',
},
},
data() {
return {
......@@ -86,7 +91,9 @@ export default {
);
},
addRelatedErrorMessage() {
if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) {
if (this.itemAddFailureMessage) {
return this.itemAddFailureMessage;
} else if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) {
return addRelatedIssueErrorMap[this.issuableType];
}
// Only other failure is MAX_NUMBER_OF_CHILD_EPICS at the moment
......
......@@ -50,6 +50,7 @@ export default {
'itemAddInProgress',
'itemAddFailure',
'itemAddFailureType',
'itemAddFailureMessage',
'itemCreateInProgress',
'showAddItemForm',
'showCreateEpicForm',
......@@ -194,6 +195,7 @@ export default {
:path-id-separator="itemPathIdSeparator"
:has-error="itemAddFailure"
:item-add-failure-type="itemAddFailureType"
:item-add-failure-message="itemAddFailureMessage"
@pendingIssuableRemoveRequest="handlePendingItemRemove"
@addIssuableFormInput="handleAddItemFormInput"
@addIssuableFormBlur="handleAddItemFormBlur"
......
......@@ -307,8 +307,11 @@ export const receiveAddItemSuccess = ({ dispatch, commit, getters }, { rawItems
dispatch('setItemInputValue', '');
dispatch('toggleAddItemForm', { toggleState: false });
};
export const receiveAddItemFailure = ({ commit }, { itemAddFailureType } = {}) => {
commit(types.RECEIVE_ADD_ITEM_FAILURE, { itemAddFailureType });
export const receiveAddItemFailure = (
{ commit },
{ itemAddFailureType, itemAddFailureMessage = '' } = {},
) => {
commit(types.RECEIVE_ADD_ITEM_FAILURE, { itemAddFailureType, itemAddFailureMessage });
};
export const addItem = ({ state, dispatch, getters }) => {
dispatch('requestAddItem');
......@@ -325,20 +328,22 @@ export const addItem = ({ state, dispatch, getters }) => {
})
.catch(data => {
const { response } = data;
if (response.status === 404) {
if (response.status === httpStatusCodes.NOT_FOUND) {
dispatch('receiveAddItemFailure', { itemAddFailureType: itemAddFailureTypesMap.NOT_FOUND });
}
// Ignore 409 conflict when the issue or epic is already attached to epic
/* eslint-disable @gitlab/i18n/no-non-i18n-strings */
else if (
response.status === 409 &&
response.status === httpStatusCodes.CONFLICT &&
response.data.message === 'Epic hierarchy level too deep'
) {
dispatch('receiveAddItemFailure', {
itemAddFailureType: itemAddFailureTypesMap.MAX_NUMBER_OF_CHILD_EPICS,
});
} else {
dispatch('receiveAddItemFailure');
dispatch('receiveAddItemFailure', {
itemAddFailureMessage: response.data.message,
});
}
});
};
......
......@@ -172,9 +172,10 @@ export default {
state.itemAddInProgress = false;
state.itemsFetchResultEmpty = false;
},
[types.RECEIVE_ADD_ITEM_FAILURE](state, { itemAddFailureType }) {
[types.RECEIVE_ADD_ITEM_FAILURE](state, { itemAddFailureType, itemAddFailureMessage }) {
state.itemAddInProgress = false;
state.itemAddFailure = true;
state.itemAddFailureMessage = itemAddFailureMessage;
if (itemAddFailureType) {
state.itemAddFailureType = itemAddFailureType;
}
......
......@@ -23,6 +23,7 @@ export default () => ({
pendingReferences: [],
itemAutoCompleteSources: {},
itemAddFailureType: null,
itemAddFailureMessage: '',
// UI Flags
itemsFetchInProgress: false,
......
......@@ -326,24 +326,30 @@ module EE
def update_project_counter_caches
end
# we call this when creating a new epic (Epics::CreateService) or linking an existing one (EpicLinks::CreateService)
# when called from EpicLinks::CreateService we pass
# parent_epic - because we don't have parent attribute set on epic
# parent_group_descendants - we have preloaded them in the service and we want to prevent performance problems
# when linking a lot of issues
def valid_parent?(parent_epic: nil, parent_group_descendants: nil)
parent_epic ||= parent
self.parent = parent_epic if parent_epic
return true unless parent_epic
validate_parent(parent_group_descendants)
parent_group_descendants ||= parent_epic.group.self_and_descendants
errors.empty?
end
def validate_parent(preloaded_parent_group_and_descendants = nil)
return unless parent
return false if self == parent_epic
return false if level_depth_exceeded?(parent_epic)
return false if parent_epic.has_ancestor?(self)
return false if parent_epic.children.to_a.include?(self)
preloaded_parent_group_and_descendants ||= parent.group.self_and_descendants
parent_group_descendants.include?(group)
if self == parent
errors.add :parent, 'Cannot add an epic as a child of itself'
elsif parent.children.to_a.include?(self)
errors.add :parent, "This epic can't be added as it is already assigned to the parent"
elsif parent.has_ancestor?(self)
errors.add :parent, "This epic can't be added as it is already assigned to this epic's ancestor"
elsif !preloaded_parent_group_and_descendants.include?(group)
errors.add :parent, "This epic can't be added because parent and child epics must belong to the same group"
elsif level_depth_exceeded?(parent)
errors.add :parent, "This epic can't be added as the maximum depth of nested epics would be exceeded"
end
end
def issues_readable_by(current_user, preload: nil)
......@@ -364,13 +370,6 @@ module EE
super.merge(label_url_method: :group_epics_url)
end
def validate_parent
return true if valid_parent?
errors.add :parent, 'The parent is not valid'
end
private :validate_parent
def level_depth_exceeded?(parent_epic)
hierarchy.max_descendants_depth.to_i + parent_epic.base_and_ancestors.count >= MAX_HIERARCHY_DEPTH
end
......
......@@ -4,14 +4,31 @@ module EpicLinks
class CreateService < IssuableLinks::CreateService
def execute
if issuable.max_hierarchy_depth_achieved?
return error('Epic hierarchy level too deep', 409)
return error("This epic can't be added because the parent is already at the maximum depth from its most distant ancestor", 409)
end
if referenced_issuables.count == 1
create_single_link
else
super
end
end
private
def create_single_link
child_epic = referenced_issuables.first
if linkable_epic?(child_epic)
set_child_epic!(child_epic)
create_notes(child_epic, nil)
success
else
error(child_epic.errors.messages[:parent].first, 409)
end
end
def affected_epics(epics)
[issuable, epics].flatten.uniq
end
......
......@@ -199,7 +199,7 @@ describe 'Epic Issues', :js do
add_epics(references)
expect(page).to have_selector('.gl-field-error')
expect(find('.gl-field-error')).to have_text("This epic already has the maximum number of child epics.")
expect(find('.gl-field-error')).to have_text("This epic can't be added because the parent is already at the maximum depth from its most distant ancestor")
end
end
......
......@@ -479,10 +479,14 @@ describe('RelatedItemsTree', () => {
describe(types.RECEIVE_ADD_ITEM_FAILURE, () => {
it('should set `itemAddInProgress` to false, `itemAddFailure` to true and `itemAddFailureType` value within state', () => {
mutations[types.RECEIVE_ADD_ITEM_FAILURE](state, { itemAddFailureType: 'bar' });
mutations[types.RECEIVE_ADD_ITEM_FAILURE](state, {
itemAddFailureMessage: 'foo',
itemAddFailureType: 'bar',
});
expect(state.itemAddInProgress).toBe(false);
expect(state.itemAddFailure).toBe(true);
expect(state.itemAddFailureMessage).toEqual('foo');
expect(state.itemAddFailureType).toEqual('bar');
});
});
......
......@@ -209,6 +209,20 @@ describe('AddIssuableForm', () => {
done();
});
});
it('shows error message when error is present', done => {
const itemAddFailureMessage = 'Something went wrong while submitting.';
wrapper.setProps({
hasError: true,
itemAddFailureMessage,
});
wrapper.vm.$nextTick(() => {
expect(wrapper.find('.gl-field-error').exists()).toBe(true);
expect(wrapper.find('.gl-field-error').text()).toContain(itemAddFailureMessage);
done();
});
});
});
});
});
......@@ -919,12 +919,18 @@ describe('RelatedItemTree', () => {
it('should set `state.itemAddInProgress` to false', done => {
testAction(
actions.receiveAddItemFailure,
{ itemAddFailureType: itemAddFailureTypesMap.NOT_FOUND },
{
itemAddFailureType: itemAddFailureTypesMap.NOT_FOUND,
itemAddFailureMessage: 'Foobar',
},
{},
[
{
type: types.RECEIVE_ADD_ITEM_FAILURE,
payload: { itemAddFailureType: itemAddFailureTypesMap.NOT_FOUND },
payload: {
itemAddFailureType: itemAddFailureTypesMap.NOT_FOUND,
itemAddFailureMessage: 'Foobar',
},
},
],
[],
......@@ -940,7 +946,7 @@ describe('RelatedItemTree', () => {
[
{
type: types.RECEIVE_ADD_ITEM_FAILURE,
payload: { itemAddFailureType: undefined },
payload: { itemAddFailureType: undefined, itemAddFailureMessage: '' },
},
],
[],
......@@ -1004,7 +1010,9 @@ describe('RelatedItemTree', () => {
},
{
type: 'receiveAddItemFailure',
payload: { itemAddFailureType: itemAddFailureTypesMap.NOT_FOUND },
payload: {
itemAddFailureType: itemAddFailureTypesMap.NOT_FOUND,
},
},
],
done,
......
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