Commit d63a1900 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '213363-better-node-errors' into 'master'

Better Geo Node Error Messages MVC

Closes #213363

See merge request gitlab-org/gitlab!29079
parents 7822f551 13800525
import Api from '~/api'; import Api from 'ee/api';
import ApiEE from 'ee/api'; import { flatten } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
const getSaveErrorMessageParts = messages => {
return flatten(
Object.entries(messages || {}).map(([key, value]) => value.map(x => `${key} ${x}`)),
);
};
const getSaveErrorMessage = messages => {
const parts = getSaveErrorMessageParts(messages);
return `${__('Errors:')} ${parts.join(', ')}`;
};
export const requestSyncNamespaces = ({ commit }) => commit(types.REQUEST_SYNC_NAMESPACES); export const requestSyncNamespaces = ({ commit }) => commit(types.REQUEST_SYNC_NAMESPACES);
export const receiveSyncNamespacesSuccess = ({ commit }, data) => export const receiveSyncNamespacesSuccess = ({ commit }, data) =>
commit(types.RECEIVE_SYNC_NAMESPACES_SUCCESS, data); commit(types.RECEIVE_SYNC_NAMESPACES_SUCCESS, data);
...@@ -31,8 +42,14 @@ export const receiveSaveGeoNodeSuccess = ({ commit }) => { ...@@ -31,8 +42,14 @@ export const receiveSaveGeoNodeSuccess = ({ commit }) => {
commit(types.RECEIVE_SAVE_GEO_NODE_COMPLETE); commit(types.RECEIVE_SAVE_GEO_NODE_COMPLETE);
visitUrl('/admin/geo/nodes'); visitUrl('/admin/geo/nodes');
}; };
export const receiveSaveGeoNodeError = ({ commit }) => { export const receiveSaveGeoNodeError = ({ commit }, data) => {
createFlash(__(`There was an error saving this Geo Node`)); let errorMessage = __('There was an error saving this Geo Node.');
if (data?.message) {
errorMessage += ` ${getSaveErrorMessage(data.message)}`;
}
createFlash(errorMessage);
commit(types.RECEIVE_SAVE_GEO_NODE_COMPLETE); commit(types.RECEIVE_SAVE_GEO_NODE_COMPLETE);
}; };
...@@ -41,9 +58,9 @@ export const saveGeoNode = ({ dispatch }, node) => { ...@@ -41,9 +58,9 @@ export const saveGeoNode = ({ dispatch }, node) => {
const sanitizedNode = convertObjectPropsToSnakeCase(node); const sanitizedNode = convertObjectPropsToSnakeCase(node);
const saveFunc = node.id ? 'updateGeoNode' : 'createGeoNode'; const saveFunc = node.id ? 'updateGeoNode' : 'createGeoNode';
ApiEE[saveFunc](sanitizedNode) Api[saveFunc](sanitizedNode)
.then(() => dispatch('receiveSaveGeoNodeSuccess')) .then(() => dispatch('receiveSaveGeoNodeSuccess'))
.catch(() => { .catch(({ response }) => {
dispatch('receiveSaveGeoNodeError'); dispatch('receiveSaveGeoNodeError', response.data);
}); });
}; };
---
title: Improve Geo Node save error messages
merge_request: 29079
author:
type: changed
...@@ -163,7 +163,7 @@ describe 'admin Geo Nodes', :js, :geo do ...@@ -163,7 +163,7 @@ describe 'admin Geo Nodes', :js, :geo do
wait_for_requests wait_for_requests
expect(current_path).to eq new_admin_geo_node_path expect(current_path).to eq new_admin_geo_node_path
expect(page).to have_content('There was an error saving this Geo Node') expect(page).to have_content(/There was an error saving this Geo Node.*primary node already exists/)
end end
end end
end end
......
...@@ -62,3 +62,8 @@ export const MOCK_NODE = { ...@@ -62,3 +62,8 @@ export const MOCK_NODE = {
minimumReverificationInterval: 7, minimumReverificationInterval: 7,
syncObjectStorage: false, syncObjectStorage: false,
}; };
export const MOCK_ERROR_MESSAGE = {
name: ["can't be blank"],
url: ["can't be blank", 'must be a valid URL'],
};
...@@ -6,7 +6,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; ...@@ -6,7 +6,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import * as actions from 'ee/geo_node_form/store/actions'; import * as actions from 'ee/geo_node_form/store/actions';
import * as types from 'ee/geo_node_form/store/mutation_types'; import * as types from 'ee/geo_node_form/store/mutation_types';
import createState from 'ee/geo_node_form/store/state'; import createState from 'ee/geo_node_form/store/state';
import { MOCK_SYNC_NAMESPACES, MOCK_NODE } from '../mock_data'; import { MOCK_SYNC_NAMESPACES, MOCK_NODE, MOCK_ERROR_MESSAGE } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
...@@ -37,39 +37,70 @@ describe('GeoNodeForm Store Actions', () => { ...@@ -37,39 +37,70 @@ describe('GeoNodeForm Store Actions', () => {
}); });
describe.each` describe.each`
action | data | mutationName | mutationCall | callback action | data | mutationName | mutationCall | callback
${actions.requestSyncNamespaces} | ${null} | ${types.REQUEST_SYNC_NAMESPACES} | ${{ type: types.REQUEST_SYNC_NAMESPACES }} | ${noCallback} ${actions.requestSyncNamespaces} | ${null} | ${types.REQUEST_SYNC_NAMESPACES} | ${{ type: types.REQUEST_SYNC_NAMESPACES }} | ${noCallback}
${actions.receiveSyncNamespacesSuccess} | ${MOCK_SYNC_NAMESPACES} | ${types.RECEIVE_SYNC_NAMESPACES_SUCCESS} | ${{ type: types.RECEIVE_SYNC_NAMESPACES_SUCCESS, payload: MOCK_SYNC_NAMESPACES }} | ${noCallback} ${actions.receiveSyncNamespacesSuccess} | ${MOCK_SYNC_NAMESPACES} | ${types.RECEIVE_SYNC_NAMESPACES_SUCCESS} | ${{ type: types.RECEIVE_SYNC_NAMESPACES_SUCCESS, payload: MOCK_SYNC_NAMESPACES }} | ${noCallback}
${actions.receiveSyncNamespacesError} | ${null} | ${types.RECEIVE_SYNC_NAMESPACES_ERROR} | ${{ type: types.RECEIVE_SYNC_NAMESPACES_ERROR }} | ${flashCallback} ${actions.receiveSyncNamespacesError} | ${null} | ${types.RECEIVE_SYNC_NAMESPACES_ERROR} | ${{ type: types.RECEIVE_SYNC_NAMESPACES_ERROR }} | ${flashCallback}
${actions.requestSaveGeoNode} | ${null} | ${types.REQUEST_SAVE_GEO_NODE} | ${{ type: types.REQUEST_SAVE_GEO_NODE }} | ${noCallback} ${actions.requestSaveGeoNode} | ${null} | ${types.REQUEST_SAVE_GEO_NODE} | ${{ type: types.REQUEST_SAVE_GEO_NODE }} | ${noCallback}
${actions.receiveSaveGeoNodeSuccess} | ${null} | ${types.RECEIVE_SAVE_GEO_NODE_COMPLETE} | ${{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }} | ${visitUrlCallback} ${actions.receiveSaveGeoNodeSuccess} | ${null} | ${types.RECEIVE_SAVE_GEO_NODE_COMPLETE} | ${{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }} | ${visitUrlCallback}
${actions.receiveSaveGeoNodeError} | ${null} | ${types.RECEIVE_SAVE_GEO_NODE_COMPLETE} | ${{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }} | ${flashCallback} ${actions.receiveSaveGeoNodeError} | ${{ message: MOCK_ERROR_MESSAGE }} | ${types.RECEIVE_SAVE_GEO_NODE_COMPLETE} | ${{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }} | ${flashCallback}
`(`non-axios calls`, ({ action, data, mutationName, mutationCall, callback }) => { `(`non-axios calls`, ({ action, data, mutationName, mutationCall, callback }) => {
describe(action.name, () => { describe(action.name, () => {
it(`should commit mutation ${mutationName}`, () => { it(`should commit mutation ${mutationName}`, () => {
testAction(action, data, state, [mutationCall], [], callback); return testAction(action, data, state, [mutationCall], []).then(() => callback());
}); });
}); });
}); });
describe.each` describe.each`
action | axiosMock | data | type | actionCalls action | axiosMock | data | type | actionCalls
${actions.fetchSyncNamespaces} | ${{ method: 'onGet', code: 200, res: MOCK_SYNC_NAMESPACES }} | ${null} | ${'success'} | ${[{ type: 'requestSyncNamespaces' }, { type: 'receiveSyncNamespacesSuccess', payload: MOCK_SYNC_NAMESPACES }]} ${actions.fetchSyncNamespaces} | ${{ method: 'onGet', code: 200, res: MOCK_SYNC_NAMESPACES }} | ${null} | ${'success'} | ${[{ type: 'requestSyncNamespaces' }, { type: 'receiveSyncNamespacesSuccess', payload: MOCK_SYNC_NAMESPACES }]}
${actions.fetchSyncNamespaces} | ${{ method: 'onGet', code: 500, res: null }} | ${null} | ${'error'} | ${[{ type: 'requestSyncNamespaces' }, { type: 'receiveSyncNamespacesError' }]} ${actions.fetchSyncNamespaces} | ${{ method: 'onGet', code: 500, res: null }} | ${null} | ${'error'} | ${[{ type: 'requestSyncNamespaces' }, { type: 'receiveSyncNamespacesError' }]}
${actions.saveGeoNode} | ${{ method: 'onPost', code: 200, res: { ...MOCK_NODE, id: null } }} | ${{ ...MOCK_NODE, id: null }} | ${'success'} | ${[{ type: 'requestSaveGeoNode' }, { type: 'receiveSaveGeoNodeSuccess' }]} ${actions.saveGeoNode} | ${{ method: 'onPost', code: 200, res: { ...MOCK_NODE, id: null } }} | ${{ ...MOCK_NODE, id: null }} | ${'success'} | ${[{ type: 'requestSaveGeoNode' }, { type: 'receiveSaveGeoNodeSuccess' }]}
${actions.saveGeoNode} | ${{ method: 'onPost', code: 500, res: null }} | ${{ ...MOCK_NODE, id: null }} | ${'error'} | ${[{ type: 'requestSaveGeoNode' }, { type: 'receiveSaveGeoNodeError' }]} ${actions.saveGeoNode} | ${{ method: 'onPost', code: 500, res: { message: MOCK_ERROR_MESSAGE } }} | ${{ ...MOCK_NODE, id: null }} | ${'error'} | ${[{ type: 'requestSaveGeoNode' }, { type: 'receiveSaveGeoNodeError', payload: { message: MOCK_ERROR_MESSAGE } }]}
${actions.saveGeoNode} | ${{ method: 'onPut', code: 200, res: MOCK_NODE }} | ${MOCK_NODE} | ${'success'} | ${[{ type: 'requestSaveGeoNode' }, { type: 'receiveSaveGeoNodeSuccess' }]} ${actions.saveGeoNode} | ${{ method: 'onPut', code: 200, res: MOCK_NODE }} | ${MOCK_NODE} | ${'success'} | ${[{ type: 'requestSaveGeoNode' }, { type: 'receiveSaveGeoNodeSuccess' }]}
${actions.saveGeoNode} | ${{ method: 'onPut', code: 500, res: null }} | ${MOCK_NODE} | ${'error'} | ${[{ type: 'requestSaveGeoNode' }, { type: 'receiveSaveGeoNodeError' }]} ${actions.saveGeoNode} | ${{ method: 'onPut', code: 500, res: { message: MOCK_ERROR_MESSAGE } }} | ${MOCK_NODE} | ${'error'} | ${[{ type: 'requestSaveGeoNode' }, { type: 'receiveSaveGeoNodeError', payload: { message: MOCK_ERROR_MESSAGE } }]}
`(`axios calls`, ({ action, axiosMock, data, type, actionCalls }) => { `(`axios calls`, ({ action, axiosMock, data, type, actionCalls }) => {
describe(action.name, () => { describe(action.name, () => {
describe(`on ${type}`, () => { describe(`on ${type}`, () => {
beforeEach(() => { beforeEach(() => {
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res); mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
}); });
it(`should dispatch the correct request and actions`, done => { it(`should dispatch the correct request and actions`, () => {
testAction(action, data, state, [], actionCalls, done); return testAction(action, data, state, [], actionCalls);
}); });
}); });
}); });
}); });
describe('receiveSaveGeoNodeError', () => {
const defaultErrorMessage = 'There was an error saving this Geo Node.';
it('when message passed it builds the error message correctly', () => {
return testAction(
actions.receiveSaveGeoNodeError,
{ message: MOCK_ERROR_MESSAGE },
state,
[{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }],
[],
).then(() => {
const errors = "Errors: name can't be blank, url can't be blank, url must be a valid URL";
expect(flash).toHaveBeenCalledWith(`${defaultErrorMessage} ${errors}`);
flash.mockClear();
});
});
it('when no data is passed it defaults the error message', () => {
return testAction(
actions.receiveSaveGeoNodeError,
null,
state,
[{ type: types.RECEIVE_SAVE_GEO_NODE_COMPLETE }],
[],
).then(() => {
expect(flash).toHaveBeenCalledWith(defaultErrorMessage);
flash.mockClear();
});
});
});
}); });
...@@ -8384,6 +8384,9 @@ msgstr "" ...@@ -8384,6 +8384,9 @@ msgstr ""
msgid "Errors" msgid "Errors"
msgstr "" msgstr ""
msgid "Errors:"
msgstr ""
msgid "Estimated" msgid "Estimated"
msgstr "" msgstr ""
...@@ -20774,7 +20777,7 @@ msgstr "" ...@@ -20774,7 +20777,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes." msgid "There was an error resetting user pipeline minutes."
msgstr "" msgstr ""
msgid "There was an error saving this Geo Node" msgid "There was an error saving this Geo Node."
msgstr "" msgstr ""
msgid "There was an error saving your changes." msgid "There was an error saving your changes."
......
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