Commit 36a98640 authored by Enrique Alcántara's avatar Enrique Alcántara Committed by Natalia Tepluhina

Display submit changes error alert with retry btn

Display a dismissable alert when submitting changes
fails in the Static Site Editor. Allow to retry
submitting changes when the error happens.
parent 7b5ad03d
...@@ -6,6 +6,7 @@ import EditArea from './edit_area.vue'; ...@@ -6,6 +6,7 @@ import EditArea from './edit_area.vue';
import EditHeader from './edit_header.vue'; import EditHeader from './edit_header.vue';
import Toolbar from './publish_toolbar.vue'; import Toolbar from './publish_toolbar.vue';
import InvalidContentMessage from './invalid_content_message.vue'; import InvalidContentMessage from './invalid_content_message.vue';
import SubmitChangesError from './submit_changes_error.vue';
export default { export default {
components: { components: {
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
InvalidContentMessage, InvalidContentMessage,
GlSkeletonLoader, GlSkeletonLoader,
Toolbar, Toolbar,
SubmitChangesError,
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -24,6 +26,7 @@ export default { ...@@ -24,6 +26,7 @@ export default {
'isSupportedContent', 'isSupportedContent',
'returnUrl', 'returnUrl',
'title', 'title',
'submitChangesError',
]), ]),
...mapGetters(['contentChanged']), ...mapGetters(['contentChanged']),
}, },
...@@ -33,7 +36,7 @@ export default { ...@@ -33,7 +36,7 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(['loadContent', 'setContent', 'submitChanges']), ...mapActions(['loadContent', 'setContent', 'submitChanges', 'dismissSubmitChangesError']),
}, },
}; };
</script> </script>
...@@ -51,6 +54,13 @@ export default { ...@@ -51,6 +54,13 @@ export default {
</gl-skeleton-loader> </gl-skeleton-loader>
</div> </div>
<div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column"> <div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column">
<submit-changes-error
v-if="submitChangesError"
class="w-75 align-self-center"
:error="submitChangesError"
@retry="submitChanges"
@dismiss="dismissSubmitChangesError"
/>
<edit-header class="w-75 align-self-center py-2" :title="title" /> <edit-header class="w-75 align-self-center py-2" :title="title" />
<edit-area <edit-area
class="w-75 h-100 shadow-none align-self-center" class="w-75 h-100 shadow-none align-self-center"
......
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
export default {
components: {
GlAlert,
GlButton,
},
props: {
error: {
type: String,
required: true,
},
},
};
</script>
<template>
<gl-alert variant="danger" dismissible @dismiss="$emit('dismiss')">
{{ s__('StaticSiteEditor|An error occurred while submitting your changes.') }} {{ error }}
<template #actions>
<gl-button variant="danger" @click="$emit('retry')">{{ __('Retry') }}</gl-button>
</template>
</gl-alert>
</template>
...@@ -26,9 +26,12 @@ export const submitChanges = ({ state: { projectId, content, sourcePath, usernam ...@@ -26,9 +26,12 @@ export const submitChanges = ({ state: { projectId, content, sourcePath, usernam
return submitContentChanges({ content, projectId, sourcePath, username }) return submitContentChanges({ content, projectId, sourcePath, username })
.then(data => commit(mutationTypes.SUBMIT_CHANGES_SUCCESS, data)) .then(data => commit(mutationTypes.SUBMIT_CHANGES_SUCCESS, data))
.catch(error => { .catch(error => {
commit(mutationTypes.SUBMIT_CHANGES_ERROR); commit(mutationTypes.SUBMIT_CHANGES_ERROR, error.message);
createFlash(error.message);
}); });
}; };
export const dismissSubmitChangesError = ({ commit }) => {
commit(mutationTypes.DISMISS_SUBMIT_CHANGES_ERROR);
};
export default () => {}; export default () => {};
...@@ -5,3 +5,4 @@ export const SET_CONTENT = 'setContent'; ...@@ -5,3 +5,4 @@ export const SET_CONTENT = 'setContent';
export const SUBMIT_CHANGES = 'submitChanges'; export const SUBMIT_CHANGES = 'submitChanges';
export const SUBMIT_CHANGES_SUCCESS = 'submitChangesSuccess'; export const SUBMIT_CHANGES_SUCCESS = 'submitChangesSuccess';
export const SUBMIT_CHANGES_ERROR = 'submitChangesError'; export const SUBMIT_CHANGES_ERROR = 'submitChangesError';
export const DISMISS_SUBMIT_CHANGES_ERROR = 'dismissSubmitChangesError';
...@@ -19,13 +19,18 @@ export default { ...@@ -19,13 +19,18 @@ export default {
}, },
[types.SUBMIT_CHANGES](state) { [types.SUBMIT_CHANGES](state) {
state.isSavingChanges = true; state.isSavingChanges = true;
state.submitChangesError = '';
}, },
[types.SUBMIT_CHANGES_SUCCESS](state, meta) { [types.SUBMIT_CHANGES_SUCCESS](state, meta) {
state.savedContentMeta = meta; state.savedContentMeta = meta;
state.isSavingChanges = false; state.isSavingChanges = false;
state.originalContent = state.content; state.originalContent = state.content;
}, },
[types.SUBMIT_CHANGES_ERROR](state) { [types.SUBMIT_CHANGES_ERROR](state, error) {
state.submitChangesError = error;
state.isSavingChanges = false; state.isSavingChanges = false;
}, },
[types.DISMISS_SUBMIT_CHANGES_ERROR](state) {
state.submitChangesError = '';
},
}; };
...@@ -14,6 +14,7 @@ const createState = (initialState = {}) => ({ ...@@ -14,6 +14,7 @@ const createState = (initialState = {}) => ({
content: '', content: '',
title: '', title: '',
submitChangesError: '',
savedContentMeta: null, savedContentMeta: null,
...initialState, ...initialState,
......
---
title: Allow to retry submitting changes when an error occurs
merge_request: 29434
author:
type: changed
...@@ -19496,6 +19496,9 @@ msgstr "" ...@@ -19496,6 +19496,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)" msgid "Static Application Security Testing (SAST)"
msgstr "" msgstr ""
msgid "StaticSiteEditor|An error occurred while submitting your changes."
msgstr ""
msgid "StaticSiteEditor|Branch could not be created." msgid "StaticSiteEditor|Branch could not be created."
msgstr "" msgstr ""
......
...@@ -10,8 +10,9 @@ import EditArea from '~/static_site_editor/components/edit_area.vue'; ...@@ -10,8 +10,9 @@ import EditArea from '~/static_site_editor/components/edit_area.vue';
import EditHeader from '~/static_site_editor/components/edit_header.vue'; import EditHeader from '~/static_site_editor/components/edit_header.vue';
import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
import { sourceContent, sourceContentTitle } from '../mock_data'; import { sourceContent, sourceContentTitle, submitChangesError } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -23,11 +24,13 @@ describe('StaticSiteEditor', () => { ...@@ -23,11 +24,13 @@ describe('StaticSiteEditor', () => {
let loadContentActionMock; let loadContentActionMock;
let setContentActionMock; let setContentActionMock;
let submitChangesActionMock; let submitChangesActionMock;
let dismissSubmitChangesErrorActionMock;
const buildStore = ({ initialState, getters } = {}) => { const buildStore = ({ initialState, getters } = {}) => {
loadContentActionMock = jest.fn(); loadContentActionMock = jest.fn();
setContentActionMock = jest.fn(); setContentActionMock = jest.fn();
submitChangesActionMock = jest.fn(); submitChangesActionMock = jest.fn();
dismissSubmitChangesErrorActionMock = jest.fn();
store = new Vuex.Store({ store = new Vuex.Store({
state: createState({ state: createState({
...@@ -42,6 +45,7 @@ describe('StaticSiteEditor', () => { ...@@ -42,6 +45,7 @@ describe('StaticSiteEditor', () => {
loadContent: loadContentActionMock, loadContent: loadContentActionMock,
setContent: setContentActionMock, setContent: setContentActionMock,
submitChanges: submitChangesActionMock, submitChanges: submitChangesActionMock,
dismissSubmitChangesError: dismissSubmitChangesErrorActionMock,
}, },
}); });
}; };
...@@ -69,6 +73,7 @@ describe('StaticSiteEditor', () => { ...@@ -69,6 +73,7 @@ describe('StaticSiteEditor', () => {
const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage); const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
const findPublishToolbar = () => wrapper.find(PublishToolbar); const findPublishToolbar = () => wrapper.find(PublishToolbar);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
beforeEach(() => { beforeEach(() => {
buildStore(); buildStore();
...@@ -145,6 +150,13 @@ describe('StaticSiteEditor', () => { ...@@ -145,6 +150,13 @@ describe('StaticSiteEditor', () => {
expect(findSkeletonLoader().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
}); });
it('does not display submit changes error when an error does not exist', () => {
buildContentLoadedStore();
buildWrapper();
expect(findSubmitChangesError().exists()).toBe(false);
});
it('sets toolbar as saving when saving changes', () => { it('sets toolbar as saving when saving changes', () => {
buildContentLoadedStore({ buildContentLoadedStore({
initialState: { initialState: {
...@@ -163,6 +175,33 @@ describe('StaticSiteEditor', () => { ...@@ -163,6 +175,33 @@ describe('StaticSiteEditor', () => {
expect(findInvalidContentMessage().exists()).toBe(true); expect(findInvalidContentMessage().exists()).toBe(true);
}); });
describe('when submitting changes fail', () => {
beforeEach(() => {
buildContentLoadedStore({
initialState: {
submitChangesError,
},
});
buildWrapper();
});
it('displays submit changes error message', () => {
expect(findSubmitChangesError().exists()).toBe(true);
});
it('dispatches submitChanges action when error message emits retry event', () => {
findSubmitChangesError().vm.$emit('retry');
expect(submitChangesActionMock).toHaveBeenCalled();
});
it('dispatches dismissSubmitChangesError action when error message emits dismiss event', () => {
findSubmitChangesError().vm.$emit('dismiss');
expect(dismissSubmitChangesErrorActionMock).toHaveBeenCalled();
});
});
it('dispatches load content action', () => { it('dispatches load content action', () => {
expect(loadContentActionMock).toHaveBeenCalled(); expect(loadContentActionMock).toHaveBeenCalled();
}); });
......
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlAlert } from '@gitlab/ui';
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
import { submitChangesError as error } from '../mock_data';
describe('Submit Changes Error', () => {
let wrapper;
const buildWrapper = (propsData = {}) => {
wrapper = shallowMount(SubmitChangesError, {
propsData: {
...propsData,
},
stubs: {
GlAlert,
},
});
};
const findRetryButton = () => wrapper.find(GlButton);
const findAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
buildWrapper({ error });
});
afterEach(() => {
wrapper.destroy();
});
it('renders error message', () => {
expect(findAlert().text()).toContain(error);
});
it('emits dismiss event when alert emits dismiss event', () => {
findAlert().vm.$emit('dismiss');
expect(wrapper.emitted('dismiss')).toHaveLength(1);
});
it('emits retry event when retry button is clicked', () => {
findRetryButton().vm.$emit('click');
expect(wrapper.emitted('retry')).toHaveLength(1);
});
});
...@@ -124,24 +124,29 @@ describe('Static Site Editor Store actions', () => { ...@@ -124,24 +124,29 @@ describe('Static Site Editor Store actions', () => {
}); });
describe('on error', () => { describe('on error', () => {
const error = new Error(submitChangesError);
const expectedMutations = [ const expectedMutations = [
{ type: mutationTypes.SUBMIT_CHANGES }, { type: mutationTypes.SUBMIT_CHANGES },
{ type: mutationTypes.SUBMIT_CHANGES_ERROR }, { type: mutationTypes.SUBMIT_CHANGES_ERROR, payload: error.message },
]; ];
beforeEach(() => { beforeEach(() => {
submitContentChanges.mockRejectedValueOnce(new Error(submitChangesError)); submitContentChanges.mockRejectedValueOnce(error);
}); });
it('dispatches receiveContentError', () => { it('dispatches receiveContentError', () => {
testAction(actions.submitChanges, null, state, expectedMutations); testAction(actions.submitChanges, null, state, expectedMutations);
}); });
});
});
it('displays flash communicating error', () => { describe('dismissSubmitChangesError', () => {
return testAction(actions.submitChanges, null, state, expectedMutations).then(() => { it('commits dismissSubmitChangesError', () => {
expect(createFlash).toHaveBeenCalledWith(submitChangesError); testAction(actions.dismissSubmitChangesError, null, state, [
}); {
}); type: mutationTypes.DISMISS_SUBMIT_CHANGES_ERROR,
},
]);
}); });
}); });
}); });
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
sourceContentTitle as title, sourceContentTitle as title,
sourceContent as content, sourceContent as content,
savedContentMeta, savedContentMeta,
submitChangesError,
} from '../mock_data'; } from '../mock_data';
describe('Static Site Editor Store mutations', () => { describe('Static Site Editor Store mutations', () => {
...@@ -16,19 +17,21 @@ describe('Static Site Editor Store mutations', () => { ...@@ -16,19 +17,21 @@ describe('Static Site Editor Store mutations', () => {
}); });
it.each` it.each`
mutation | stateProperty | payload | expectedValue mutation | stateProperty | payload | expectedValue
${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true} ${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false} ${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true} ${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title} ${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title}
${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content} ${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content} ${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false} ${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false}
${types.SET_CONTENT} | ${'content'} | ${content} | ${content} ${types.SET_CONTENT} | ${'content'} | ${content} | ${content}
${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true} ${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true}
${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta} ${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta}
${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${false} ${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${false}
${types.SUBMIT_CHANGES_ERROR} | ${'isSavingChanges'} | ${undefined} | ${false} ${types.SUBMIT_CHANGES_ERROR} | ${'isSavingChanges'} | ${undefined} | ${false}
${types.SUBMIT_CHANGES_ERROR} | ${'submitChangesError'} | ${submitChangesError} | ${submitChangesError}
${types.DISMISS_SUBMIT_CHANGES_ERROR} | ${'submitChangesError'} | ${undefined} | ${''}
`( `(
'$mutation sets $stateProperty to $expectedValue', '$mutation sets $stateProperty to $expectedValue',
({ mutation, stateProperty, payload, expectedValue }) => { ({ mutation, stateProperty, payload, expectedValue }) => {
......
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