Commit 1c75f002 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'display-retry-button-in-error-message' into 'master'

Allow to retry submitting changes when an error occurs

See merge request gitlab-org/gitlab!29434
parents dc2094dd 36a98640
......@@ -6,6 +6,7 @@ import EditArea from './edit_area.vue';
import EditHeader from './edit_header.vue';
import Toolbar from './publish_toolbar.vue';
import InvalidContentMessage from './invalid_content_message.vue';
import SubmitChangesError from './submit_changes_error.vue';
export default {
components: {
......@@ -14,6 +15,7 @@ export default {
InvalidContentMessage,
GlSkeletonLoader,
Toolbar,
SubmitChangesError,
},
computed: {
...mapState([
......@@ -24,6 +26,7 @@ export default {
'isSupportedContent',
'returnUrl',
'title',
'submitChangesError',
]),
...mapGetters(['contentChanged']),
},
......@@ -33,7 +36,7 @@ export default {
}
},
methods: {
...mapActions(['loadContent', 'setContent', 'submitChanges']),
...mapActions(['loadContent', 'setContent', 'submitChanges', 'dismissSubmitChangesError']),
},
};
</script>
......@@ -51,6 +54,13 @@ export default {
</gl-skeleton-loader>
</div>
<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-area
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
return submitContentChanges({ content, projectId, sourcePath, username })
.then(data => commit(mutationTypes.SUBMIT_CHANGES_SUCCESS, data))
.catch(error => {
commit(mutationTypes.SUBMIT_CHANGES_ERROR);
createFlash(error.message);
commit(mutationTypes.SUBMIT_CHANGES_ERROR, error.message);
});
};
export const dismissSubmitChangesError = ({ commit }) => {
commit(mutationTypes.DISMISS_SUBMIT_CHANGES_ERROR);
};
export default () => {};
......@@ -5,3 +5,4 @@ export const SET_CONTENT = 'setContent';
export const SUBMIT_CHANGES = 'submitChanges';
export const SUBMIT_CHANGES_SUCCESS = 'submitChangesSuccess';
export const SUBMIT_CHANGES_ERROR = 'submitChangesError';
export const DISMISS_SUBMIT_CHANGES_ERROR = 'dismissSubmitChangesError';
......@@ -19,13 +19,18 @@ export default {
},
[types.SUBMIT_CHANGES](state) {
state.isSavingChanges = true;
state.submitChangesError = '';
},
[types.SUBMIT_CHANGES_SUCCESS](state, meta) {
state.savedContentMeta = meta;
state.isSavingChanges = false;
state.originalContent = state.content;
},
[types.SUBMIT_CHANGES_ERROR](state) {
[types.SUBMIT_CHANGES_ERROR](state, error) {
state.submitChangesError = error;
state.isSavingChanges = false;
},
[types.DISMISS_SUBMIT_CHANGES_ERROR](state) {
state.submitChangesError = '';
},
};
......@@ -14,6 +14,7 @@ const createState = (initialState = {}) => ({
content: '',
title: '',
submitChangesError: '',
savedContentMeta: null,
...initialState,
......
---
title: Allow to retry submitting changes when an error occurs
merge_request: 29434
author:
type: changed
......@@ -19496,6 +19496,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
msgid "StaticSiteEditor|An error occurred while submitting your changes."
msgstr ""
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
......
......@@ -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 InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.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();
......@@ -23,11 +24,13 @@ describe('StaticSiteEditor', () => {
let loadContentActionMock;
let setContentActionMock;
let submitChangesActionMock;
let dismissSubmitChangesErrorActionMock;
const buildStore = ({ initialState, getters } = {}) => {
loadContentActionMock = jest.fn();
setContentActionMock = jest.fn();
submitChangesActionMock = jest.fn();
dismissSubmitChangesErrorActionMock = jest.fn();
store = new Vuex.Store({
state: createState({
......@@ -42,6 +45,7 @@ describe('StaticSiteEditor', () => {
loadContent: loadContentActionMock,
setContent: setContentActionMock,
submitChanges: submitChangesActionMock,
dismissSubmitChangesError: dismissSubmitChangesErrorActionMock,
},
});
};
......@@ -69,6 +73,7 @@ describe('StaticSiteEditor', () => {
const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
const findPublishToolbar = () => wrapper.find(PublishToolbar);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
beforeEach(() => {
buildStore();
......@@ -145,6 +150,13 @@ describe('StaticSiteEditor', () => {
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', () => {
buildContentLoadedStore({
initialState: {
......@@ -163,6 +175,33 @@ describe('StaticSiteEditor', () => {
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', () => {
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', () => {
});
describe('on error', () => {
const error = new Error(submitChangesError);
const expectedMutations = [
{ type: mutationTypes.SUBMIT_CHANGES },
{ type: mutationTypes.SUBMIT_CHANGES_ERROR },
{ type: mutationTypes.SUBMIT_CHANGES_ERROR, payload: error.message },
];
beforeEach(() => {
submitContentChanges.mockRejectedValueOnce(new Error(submitChangesError));
submitContentChanges.mockRejectedValueOnce(error);
});
it('dispatches receiveContentError', () => {
testAction(actions.submitChanges, null, state, expectedMutations);
});
});
});
it('displays flash communicating error', () => {
return testAction(actions.submitChanges, null, state, expectedMutations).then(() => {
expect(createFlash).toHaveBeenCalledWith(submitChangesError);
});
});
describe('dismissSubmitChangesError', () => {
it('commits dismissSubmitChangesError', () => {
testAction(actions.dismissSubmitChangesError, null, state, [
{
type: mutationTypes.DISMISS_SUBMIT_CHANGES_ERROR,
},
]);
});
});
});
......@@ -5,6 +5,7 @@ import {
sourceContentTitle as title,
sourceContent as content,
savedContentMeta,
submitChangesError,
} from '../mock_data';
describe('Static Site Editor Store mutations', () => {
......@@ -16,19 +17,21 @@ describe('Static Site Editor Store mutations', () => {
});
it.each`
mutation | stateProperty | payload | expectedValue
${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title}
${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false}
${types.SET_CONTENT} | ${'content'} | ${content} | ${content}
${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true}
${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta}
${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${false}
${types.SUBMIT_CHANGES_ERROR} | ${'isSavingChanges'} | ${undefined} | ${false}
mutation | stateProperty | payload | expectedValue
${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title}
${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false}
${types.SET_CONTENT} | ${'content'} | ${content} | ${content}
${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true}
${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta}
${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${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, 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