Commit d2ff0630 authored by Derek Knox's avatar Derek Knox Committed by Natalia Tepluhina

Initial submit changes speed

Updated success component to conditionally
show a loader for perceived speed UX boost
parent 92a40178
...@@ -41,7 +41,7 @@ export default { ...@@ -41,7 +41,7 @@ export default {
:disabled="savingChanges" :disabled="savingChanges"
@click="$emit('editSettings')" @click="$emit('editSettings')"
> >
{{ __('Settings') }} {{ __('Page settings') }}
</gl-button> </gl-button>
<gl-button <gl-button
ref="submit" ref="submit"
......
...@@ -4,6 +4,7 @@ import createDefaultClient from '~/lib/graphql'; ...@@ -4,6 +4,7 @@ import createDefaultClient from '~/lib/graphql';
import typeDefs from './typedefs.graphql'; import typeDefs from './typedefs.graphql';
import fileResolver from './resolvers/file'; import fileResolver from './resolvers/file';
import submitContentChangesResolver from './resolvers/submit_content_changes'; import submitContentChangesResolver from './resolvers/submit_content_changes';
import hasSubmittedChangesResolver from './resolvers/has_submitted_changes';
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -15,6 +16,7 @@ const createApolloProvider = appData => { ...@@ -15,6 +16,7 @@ const createApolloProvider = appData => {
}, },
Mutation: { Mutation: {
submitContentChanges: submitContentChangesResolver, submitContentChanges: submitContentChangesResolver,
hasSubmittedChanges: hasSubmittedChangesResolver,
}, },
}, },
{ {
......
mutation hasSubmittedChanges($input: HasSubmittedChangesInput) {
hasSubmittedChanges(input: $input) @client {
hasSubmittedChanges
}
}
query appData { query appData {
appData @client { appData @client {
isSupportedContent isSupportedContent
hasSubmittedChanges
project project
sourcePath sourcePath
username username
......
import query from '../queries/app_data.query.graphql';
const hasSubmittedChangesResolver = (_, { input: { hasSubmittedChanges } }, { cache }) => {
const { appData } = cache.readQuery({ query });
cache.writeQuery({
query,
data: {
appData: {
__typename: 'AppData',
...appData,
hasSubmittedChanges,
},
},
});
};
export default hasSubmittedChangesResolver;
...@@ -16,12 +16,17 @@ type SavedContentMeta { ...@@ -16,12 +16,17 @@ type SavedContentMeta {
type AppData { type AppData {
isSupportedContent: Boolean! isSupportedContent: Boolean!
hasSubmittedChanges: Boolean!
project: String! project: String!
returnUrl: String returnUrl: String
sourcePath: String! sourcePath: String!
username: String! username: String!
} }
input HasSubmittedChangesInput {
hasSubmittedChanges: Boolean!
}
input SubmitContentChangesInput { input SubmitContentChangesInput {
project: String! project: String!
sourcePath: String! sourcePath: String!
...@@ -40,4 +45,5 @@ extend type Query { ...@@ -40,4 +45,5 @@ extend type Query {
extend type Mutation { extend type Mutation {
submitContentChanges(input: SubmitContentChangesInput!): SavedContentMeta submitContentChanges(input: SubmitContentChangesInput!): SavedContentMeta
hasSubmittedChanges(input: HasSubmittedChangesInput!): AppData
} }
...@@ -19,6 +19,7 @@ const initStaticSiteEditor = el => { ...@@ -19,6 +19,7 @@ const initStaticSiteEditor = el => {
const router = createRouter(baseUrl); const router = createRouter(baseUrl);
const apolloProvider = createApolloProvider({ const apolloProvider = createApolloProvider({
isSupportedContent: parseBoolean(isSupportedContent), isSupportedContent: parseBoolean(isSupportedContent),
hasSubmittedChanges: false,
project: `${namespace}/${project}`, project: `${namespace}/${project}`,
returnUrl, returnUrl,
sourcePath, sourcePath,
......
...@@ -5,6 +5,7 @@ import InvalidContentMessage from '../components/invalid_content_message.vue'; ...@@ -5,6 +5,7 @@ import InvalidContentMessage from '../components/invalid_content_message.vue';
import SubmitChangesError from '../components/submit_changes_error.vue'; import SubmitChangesError from '../components/submit_changes_error.vue';
import appDataQuery from '../graphql/queries/app_data.query.graphql'; import appDataQuery from '../graphql/queries/app_data.query.graphql';
import sourceContentQuery from '../graphql/queries/source_content.query.graphql'; import sourceContentQuery from '../graphql/queries/source_content.query.graphql';
import hasSubmittedChangesMutation from '../graphql/mutations/has_submitted_changes.mutation.graphql';
import submitContentChangesMutation from '../graphql/mutations/submit_content_changes.mutation.graphql'; import submitContentChangesMutation from '../graphql/mutations/submit_content_changes.mutation.graphql';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
...@@ -74,6 +75,20 @@ export default { ...@@ -74,6 +75,20 @@ export default {
submitChanges(images) { submitChanges(images) {
this.isSavingChanges = true; this.isSavingChanges = true;
// eslint-disable-next-line promise/catch-or-return
this.$apollo
.mutate({
mutation: hasSubmittedChangesMutation,
variables: {
input: {
hasSubmittedChanges: true,
},
},
})
.finally(() => {
this.$router.push(SUCCESS_ROUTE);
});
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: submitContentChangesMutation, mutation: submitContentChangesMutation,
...@@ -87,9 +102,6 @@ export default { ...@@ -87,9 +102,6 @@ export default {
}, },
}, },
}) })
.then(() => {
this.$router.push(SUCCESS_ROUTE);
})
.catch(e => { .catch(e => {
this.submitChangesError = e.message; this.submitChangesError = e.message;
}) })
......
<script> <script>
import { GlEmptyState, GlButton } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql'; import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
...@@ -8,8 +8,9 @@ import { HOME_ROUTE } from '../router/constants'; ...@@ -8,8 +8,9 @@ import { HOME_ROUTE } from '../router/constants';
export default { export default {
components: { components: {
GlEmptyState,
GlButton, GlButton,
GlEmptyState,
GlLoadingIcon,
}, },
props: { props: {
mergeRequestsIllustrationPath: { mergeRequestsIllustrationPath: {
...@@ -33,7 +34,7 @@ export default { ...@@ -33,7 +34,7 @@ export default {
}, },
}, },
created() { created() {
if (!this.savedContentMeta) { if (!this.appData.hasSubmittedChanges) {
this.$router.push(HOME_ROUTE); this.$router.push(HOME_ROUTE);
} }
}, },
...@@ -50,14 +51,21 @@ export default { ...@@ -50,14 +51,21 @@ export default {
assignMergeRequestInstruction: s__( assignMergeRequestInstruction: s__(
'StaticSiteEditor|3. Assign a person to review and accept the merge request.', 'StaticSiteEditor|3. Assign a person to review and accept the merge request.',
), ),
submittingTitle: s__('StaticSiteEditor|Creating your merge request'),
submittingNotePrimary: s__(
'StaticSiteEditor|You can set an assignee to get your changes reviewed and deployed once your merge request is created.',
),
submittingNoteSecondary: s__(
'StaticSiteEditor|A link to view the merge request will appear once ready.',
),
}; };
</script> </script>
<template> <template>
<div <div class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
v-if="savedContentMeta" <div
class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column" v-if="savedContentMeta"
> class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
<div class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"> >
<div class="container gl-py-4"> <div class="container gl-py-4">
<gl-button <gl-button
v-if="appData.returnUrl" v-if="appData.returnUrl"
...@@ -73,16 +81,23 @@ export default { ...@@ -73,16 +81,23 @@ export default {
</div> </div>
<gl-empty-state <gl-empty-state
class="gl-my-9" class="gl-my-9"
:primary-button-text="$options.primaryButtonText" :title="savedContentMeta ? $options.title : $options.submittingTitle"
:title="$options.title" :primary-button-text="savedContentMeta && $options.primaryButtonText"
:primary-button-link="savedContentMeta.mergeRequest.url" :primary-button-link="savedContentMeta && savedContentMeta.mergeRequest.url"
:svg-path="mergeRequestsIllustrationPath" :svg-path="mergeRequestsIllustrationPath"
> >
<template #description> <template #description>
<p>{{ $options.mergeRequestInstructionsHeading }}</p> <div v-if="savedContentMeta">
<p>{{ $options.addTitleInstruction }}</p> <p>{{ $options.mergeRequestInstructionsHeading }}</p>
<p>{{ $options.addDescriptionInstruction }}</p> <p>{{ $options.addTitleInstruction }}</p>
<p>{{ $options.assignMergeRequestInstruction }}</p> <p>{{ $options.addDescriptionInstruction }}</p>
<p>{{ $options.assignMergeRequestInstruction }}</p>
</div>
<div v-else>
<p>{{ $options.submittingNotePrimary }}</p>
<p>{{ $options.submittingNoteSecondary }}</p>
<gl-loading-icon size="xl" />
</div>
</template> </template>
</gl-empty-state> </gl-empty-state>
</div> </div>
......
---
title: Update user feedback to a dedicated page as opposed to solely a button with a loader
merge_request: 43189
author:
type: changed
...@@ -8,8 +8,6 @@ msgid "" ...@@ -8,8 +8,6 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-22 19:32+0200\n"
"PO-Revision-Date: 2020-09-22 19:32+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -24424,6 +24422,9 @@ msgstr "" ...@@ -24424,6 +24422,9 @@ msgstr ""
msgid "StaticSiteEditor|3. Assign a person to review and accept the merge request." msgid "StaticSiteEditor|3. Assign a person to review and accept the merge request."
msgstr "" msgstr ""
msgid "StaticSiteEditor|A link to view the merge request will appear once ready."
msgstr ""
msgid "StaticSiteEditor|An error occurred while submitting your changes." msgid "StaticSiteEditor|An error occurred while submitting your changes."
msgstr "" msgstr ""
...@@ -24436,6 +24437,9 @@ msgstr "" ...@@ -24436,6 +24437,9 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request." msgid "StaticSiteEditor|Could not create merge request."
msgstr "" msgstr ""
msgid "StaticSiteEditor|Creating your merge request"
msgstr ""
msgid "StaticSiteEditor|Incompatible file content" msgid "StaticSiteEditor|Incompatible file content"
msgstr "" msgstr ""
...@@ -24457,6 +24461,9 @@ msgstr "" ...@@ -24457,6 +24461,9 @@ msgstr ""
msgid "StaticSiteEditor|View documentation" msgid "StaticSiteEditor|View documentation"
msgstr "" msgstr ""
msgid "StaticSiteEditor|You can set an assignee to get your changes reviewed and deployed once your merge request is created."
msgstr ""
msgid "StaticSiteEditor|Your merge request has been created" msgid "StaticSiteEditor|Your merge request has been created"
msgstr "" msgstr ""
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Home from '~/static_site_editor/pages/home.vue'; import Home from '~/static_site_editor/pages/home.vue';
...@@ -7,6 +6,7 @@ import EditArea from '~/static_site_editor/components/edit_area.vue'; ...@@ -7,6 +6,7 @@ import EditArea from '~/static_site_editor/components/edit_area.vue';
import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue'; import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
import submitContentChangesMutation from '~/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql'; import submitContentChangesMutation from '~/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql';
import hasSubmittedChangesMutation from '~/static_site_editor/graphql/mutations/has_submitted_changes.mutation.graphql';
import { SUCCESS_ROUTE } from '~/static_site_editor/router/constants'; import { SUCCESS_ROUTE } from '~/static_site_editor/router/constants';
import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constants'; import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constants';
...@@ -24,8 +24,6 @@ import { ...@@ -24,8 +24,6 @@ import {
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex);
describe('static_site_editor/pages/home', () => { describe('static_site_editor/pages/home', () => {
let wrapper; let wrapper;
let store; let store;
...@@ -33,6 +31,19 @@ describe('static_site_editor/pages/home', () => { ...@@ -33,6 +31,19 @@ describe('static_site_editor/pages/home', () => {
let $router; let $router;
let mutateMock; let mutateMock;
let trackingSpy; let trackingSpy;
const defaultAppData = {
isSupportedContent: true,
hasSubmittedChanges: false,
returnUrl,
project,
username,
sourcePath,
};
const hasSubmittedChangesMutationPayload = {
data: {
appData: { ...defaultAppData, hasSubmittedChanges: true },
},
};
const buildApollo = (queries = {}) => { const buildApollo = (queries = {}) => {
mutateMock = jest.fn(); mutateMock = jest.fn();
...@@ -64,7 +75,7 @@ describe('static_site_editor/pages/home', () => { ...@@ -64,7 +75,7 @@ describe('static_site_editor/pages/home', () => {
}, },
data() { data() {
return { return {
appData: { isSupportedContent: true, returnUrl, project, username, sourcePath }, appData: { ...defaultAppData },
sourceContent: { title, content }, sourceContent: { title, content },
...data, ...data,
}; };
...@@ -152,8 +163,14 @@ describe('static_site_editor/pages/home', () => { ...@@ -152,8 +163,14 @@ describe('static_site_editor/pages/home', () => {
}); });
describe('when submitting changes fails', () => { describe('when submitting changes fails', () => {
const setupMutateMock = () => {
mutateMock
.mockResolvedValueOnce(hasSubmittedChangesMutationPayload)
.mockRejectedValueOnce(new Error(submitChangesError));
};
beforeEach(() => { beforeEach(() => {
mutateMock.mockRejectedValue(new Error(submitChangesError)); setupMutateMock();
buildWrapper(); buildWrapper();
findEditArea().vm.$emit('submit', { content }); findEditArea().vm.$emit('submit', { content });
...@@ -166,6 +183,8 @@ describe('static_site_editor/pages/home', () => { ...@@ -166,6 +183,8 @@ describe('static_site_editor/pages/home', () => {
}); });
it('retries submitting changes when retry button is clicked', () => { it('retries submitting changes when retry button is clicked', () => {
setupMutateMock();
findSubmitChangesError().vm.$emit('retry'); findSubmitChangesError().vm.$emit('retry');
expect(mutateMock).toHaveBeenCalled(); expect(mutateMock).toHaveBeenCalled();
...@@ -190,7 +209,11 @@ describe('static_site_editor/pages/home', () => { ...@@ -190,7 +209,11 @@ describe('static_site_editor/pages/home', () => {
const newContent = `new ${content}`; const newContent = `new ${content}`;
beforeEach(() => { beforeEach(() => {
mutateMock.mockResolvedValueOnce({ data: { submitContentChanges: savedContentMeta } }); mutateMock.mockResolvedValueOnce(hasSubmittedChangesMutationPayload).mockResolvedValueOnce({
data: {
submitContentChanges: savedContentMeta,
},
});
buildWrapper(); buildWrapper();
findEditArea().vm.$emit('submit', { content: newContent }); findEditArea().vm.$emit('submit', { content: newContent });
...@@ -198,8 +221,19 @@ describe('static_site_editor/pages/home', () => { ...@@ -198,8 +221,19 @@ describe('static_site_editor/pages/home', () => {
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
it('dispatches hasSubmittedChanges mutation', () => {
expect(mutateMock).toHaveBeenNthCalledWith(1, {
mutation: hasSubmittedChangesMutation,
variables: {
input: {
hasSubmittedChanges: true,
},
},
});
});
it('dispatches submitContentChanges mutation', () => { it('dispatches submitContentChanges mutation', () => {
expect(mutateMock).toHaveBeenCalledWith({ expect(mutateMock).toHaveBeenNthCalledWith(2, {
mutation: submitContentChangesMutation, mutation: submitContentChangesMutation,
variables: { variables: {
input: { input: {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlButton } from '@gitlab/ui'; import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import Success from '~/static_site_editor/pages/success.vue'; import Success from '~/static_site_editor/pages/success.vue';
import { savedContentMeta, returnUrl, sourcePath } from '../mock_data'; import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
import { HOME_ROUTE } from '~/static_site_editor/router/constants'; import { HOME_ROUTE } from '~/static_site_editor/router/constants';
describe('static_site_editor/pages/success', () => { describe('~/static_site_editor/pages/success.vue', () => {
const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg'; const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
let wrapper; let wrapper;
let router; let router;
...@@ -15,14 +15,15 @@ describe('static_site_editor/pages/success', () => { ...@@ -15,14 +15,15 @@ describe('static_site_editor/pages/success', () => {
}; };
}; };
const buildWrapper = (data = {}) => { const buildWrapper = (data = {}, appData = {}) => {
wrapper = shallowMount(Success, { wrapper = shallowMount(Success, {
mocks: { mocks: {
$router: router, $router: router,
}, },
stubs: { stubs: {
GlEmptyState,
GlButton, GlButton,
GlEmptyState,
GlLoadingIcon,
}, },
propsData: { propsData: {
mergeRequestsIllustrationPath, mergeRequestsIllustrationPath,
...@@ -33,6 +34,8 @@ describe('static_site_editor/pages/success', () => { ...@@ -33,6 +34,8 @@ describe('static_site_editor/pages/success', () => {
appData: { appData: {
returnUrl, returnUrl,
sourcePath, sourcePath,
hasSubmittedChanges: true,
...appData,
}, },
...data, ...data,
}; };
...@@ -40,8 +43,9 @@ describe('static_site_editor/pages/success', () => { ...@@ -40,8 +43,9 @@ describe('static_site_editor/pages/success', () => {
}); });
}; };
const findEmptyState = () => wrapper.find(GlEmptyState);
const findReturnUrlButton = () => wrapper.find(GlButton); const findReturnUrlButton = () => wrapper.find(GlButton);
const findEmptyState = () => wrapper.find(GlEmptyState);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
beforeEach(() => { beforeEach(() => {
buildRouter(); buildRouter();
...@@ -52,50 +56,75 @@ describe('static_site_editor/pages/success', () => { ...@@ -52,50 +56,75 @@ describe('static_site_editor/pages/success', () => {
wrapper = null; wrapper = null;
}); });
it('renders empty state with a link to the created merge request', () => { describe('when savedContentMeta is valid', () => {
buildWrapper(); it('renders empty state with a link to the created merge request', () => {
buildWrapper();
expect(findEmptyState().exists()).toBe(true);
expect(findEmptyState().props()).toMatchObject({
primaryButtonText: 'View merge request',
primaryButtonLink: savedContentMeta.mergeRequest.url,
title: 'Your merge request has been created',
svgPath: mergeRequestsIllustrationPath,
});
});
expect(findEmptyState().exists()).toBe(true); it('displays merge request instructions in the empty state', () => {
expect(findEmptyState().props()).toMatchObject({ buildWrapper();
primaryButtonText: 'View merge request',
primaryButtonLink: savedContentMeta.mergeRequest.url, expect(findEmptyState().text()).toContain(
title: 'Your merge request has been created', 'To see your changes live you will need to do the following things:',
svgPath: mergeRequestsIllustrationPath, );
expect(findEmptyState().text()).toContain('1. Add a clear title to describe the change.');
expect(findEmptyState().text()).toContain(
'2. Add a description to explain why the change is being made.',
);
expect(findEmptyState().text()).toContain(
'3. Assign a person to review and accept the merge request.',
);
}); });
});
it('displays merge request instructions in the empty state', () => { it('displays return to site button', () => {
buildWrapper(); buildWrapper();
expect(findEmptyState().text()).toContain( expect(findReturnUrlButton().text()).toBe('Return to site');
'To see your changes live you will need to do the following things:', expect(findReturnUrlButton().attributes().href).toBe(returnUrl);
); });
expect(findEmptyState().text()).toContain('1. Add a clear title to describe the change.');
expect(findEmptyState().text()).toContain(
'2. Add a description to explain why the change is being made.',
);
expect(findEmptyState().text()).toContain(
'3. Assign a person to review and accept the merge request.',
);
});
it('displays return to site button', () => { it('displays source path', () => {
buildWrapper(); buildWrapper();
expect(findReturnUrlButton().text()).toBe('Return to site'); expect(wrapper.text()).toContain(`Update ${sourcePath} file`);
expect(findReturnUrlButton().attributes().href).toBe(returnUrl); });
}); });
it('displays source path', () => { describe('when savedContentMeta is invalid', () => {
buildWrapper(); it('renders empty state with a loader', () => {
buildWrapper({ savedContentMeta: null });
expect(wrapper.text()).toContain(`Update ${sourcePath} file`); expect(findEmptyState().exists()).toBe(true);
}); expect(findEmptyState().props()).toMatchObject({
title: 'Creating your merge request',
svgPath: mergeRequestsIllustrationPath,
});
expect(findLoadingIcon().exists()).toBe(true);
});
it('redirects to the HOME route when content has not been submitted', () => { it('displays helper info in the empty state', () => {
buildWrapper({ savedContentMeta: null }); buildWrapper({ savedContentMeta: null });
expect(router.push).toHaveBeenCalledWith(HOME_ROUTE); expect(findEmptyState().text()).toContain(
expect(wrapper.html()).toBe(''); 'You can set an assignee to get your changes reviewed and deployed once your merge request is created',
);
expect(findEmptyState().text()).toContain(
'A link to view the merge request will appear once ready',
);
});
it('redirects to the HOME route when content has not been submitted', () => {
buildWrapper({ savedContentMeta: null }, { hasSubmittedChanges: false });
expect(router.push).toHaveBeenCalledWith(HOME_ROUTE);
});
}); });
}); });
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