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 {
:disabled="savingChanges"
@click="$emit('editSettings')"
>
{{ __('Settings') }}
{{ __('Page settings') }}
</gl-button>
<gl-button
ref="submit"
......
......@@ -4,6 +4,7 @@ import createDefaultClient from '~/lib/graphql';
import typeDefs from './typedefs.graphql';
import fileResolver from './resolvers/file';
import submitContentChangesResolver from './resolvers/submit_content_changes';
import hasSubmittedChangesResolver from './resolvers/has_submitted_changes';
Vue.use(VueApollo);
......@@ -15,6 +16,7 @@ const createApolloProvider = appData => {
},
Mutation: {
submitContentChanges: submitContentChangesResolver,
hasSubmittedChanges: hasSubmittedChangesResolver,
},
},
{
......
mutation hasSubmittedChanges($input: HasSubmittedChangesInput) {
hasSubmittedChanges(input: $input) @client {
hasSubmittedChanges
}
}
query appData {
appData @client {
isSupportedContent
hasSubmittedChanges
project
sourcePath
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 {
type AppData {
isSupportedContent: Boolean!
hasSubmittedChanges: Boolean!
project: String!
returnUrl: String
sourcePath: String!
username: String!
}
input HasSubmittedChangesInput {
hasSubmittedChanges: Boolean!
}
input SubmitContentChangesInput {
project: String!
sourcePath: String!
......@@ -40,4 +45,5 @@ extend type Query {
extend type Mutation {
submitContentChanges(input: SubmitContentChangesInput!): SavedContentMeta
hasSubmittedChanges(input: HasSubmittedChangesInput!): AppData
}
......@@ -19,6 +19,7 @@ const initStaticSiteEditor = el => {
const router = createRouter(baseUrl);
const apolloProvider = createApolloProvider({
isSupportedContent: parseBoolean(isSupportedContent),
hasSubmittedChanges: false,
project: `${namespace}/${project}`,
returnUrl,
sourcePath,
......
......@@ -5,6 +5,7 @@ import InvalidContentMessage from '../components/invalid_content_message.vue';
import SubmitChangesError from '../components/submit_changes_error.vue';
import appDataQuery from '../graphql/queries/app_data.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 { deprecatedCreateFlash as createFlash } from '~/flash';
import Tracking from '~/tracking';
......@@ -74,6 +75,20 @@ export default {
submitChanges(images) {
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
.mutate({
mutation: submitContentChangesMutation,
......@@ -87,9 +102,6 @@ export default {
},
},
})
.then(() => {
this.$router.push(SUCCESS_ROUTE);
})
.catch(e => {
this.submitChangesError = e.message;
})
......
<script>
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
......@@ -8,8 +8,9 @@ import { HOME_ROUTE } from '../router/constants';
export default {
components: {
GlEmptyState,
GlButton,
GlEmptyState,
GlLoadingIcon,
},
props: {
mergeRequestsIllustrationPath: {
......@@ -33,7 +34,7 @@ export default {
},
},
created() {
if (!this.savedContentMeta) {
if (!this.appData.hasSubmittedChanges) {
this.$router.push(HOME_ROUTE);
}
},
......@@ -50,14 +51,21 @@ export default {
assignMergeRequestInstruction: s__(
'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>
<template>
<div
v-if="savedContentMeta"
class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column"
>
<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-flex-grow-1 gl-display-flex gl-flex-direction-column">
<div
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="container gl-py-4">
<gl-button
v-if="appData.returnUrl"
......@@ -73,16 +81,23 @@ export default {
</div>
<gl-empty-state
class="gl-my-9"
:primary-button-text="$options.primaryButtonText"
:title="$options.title"
:primary-button-link="savedContentMeta.mergeRequest.url"
:title="savedContentMeta ? $options.title : $options.submittingTitle"
:primary-button-text="savedContentMeta && $options.primaryButtonText"
:primary-button-link="savedContentMeta && savedContentMeta.mergeRequest.url"
:svg-path="mergeRequestsIllustrationPath"
>
<template #description>
<p>{{ $options.mergeRequestInstructionsHeading }}</p>
<p>{{ $options.addTitleInstruction }}</p>
<p>{{ $options.addDescriptionInstruction }}</p>
<p>{{ $options.assignMergeRequestInstruction }}</p>
<div v-if="savedContentMeta">
<p>{{ $options.mergeRequestInstructionsHeading }}</p>
<p>{{ $options.addTitleInstruction }}</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>
</gl-empty-state>
</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 ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\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"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -24424,6 +24422,9 @@ msgstr ""
msgid "StaticSiteEditor|3. Assign a person to review and accept the merge request."
msgstr ""
msgid "StaticSiteEditor|A link to view the merge request will appear once ready."
msgstr ""
msgid "StaticSiteEditor|An error occurred while submitting your changes."
msgstr ""
......@@ -24436,6 +24437,9 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
msgid "StaticSiteEditor|Creating your merge request"
msgstr ""
msgid "StaticSiteEditor|Incompatible file content"
msgstr ""
......@@ -24457,6 +24461,9 @@ msgstr ""
msgid "StaticSiteEditor|View documentation"
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"
msgstr ""
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Home from '~/static_site_editor/pages/home.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 SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
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 { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constants';
......@@ -24,8 +24,6 @@ import {
const localVue = createLocalVue();
localVue.use(Vuex);
describe('static_site_editor/pages/home', () => {
let wrapper;
let store;
......@@ -33,6 +31,19 @@ describe('static_site_editor/pages/home', () => {
let $router;
let mutateMock;
let trackingSpy;
const defaultAppData = {
isSupportedContent: true,
hasSubmittedChanges: false,
returnUrl,
project,
username,
sourcePath,
};
const hasSubmittedChangesMutationPayload = {
data: {
appData: { ...defaultAppData, hasSubmittedChanges: true },
},
};
const buildApollo = (queries = {}) => {
mutateMock = jest.fn();
......@@ -64,7 +75,7 @@ describe('static_site_editor/pages/home', () => {
},
data() {
return {
appData: { isSupportedContent: true, returnUrl, project, username, sourcePath },
appData: { ...defaultAppData },
sourceContent: { title, content },
...data,
};
......@@ -152,8 +163,14 @@ describe('static_site_editor/pages/home', () => {
});
describe('when submitting changes fails', () => {
const setupMutateMock = () => {
mutateMock
.mockResolvedValueOnce(hasSubmittedChangesMutationPayload)
.mockRejectedValueOnce(new Error(submitChangesError));
};
beforeEach(() => {
mutateMock.mockRejectedValue(new Error(submitChangesError));
setupMutateMock();
buildWrapper();
findEditArea().vm.$emit('submit', { content });
......@@ -166,6 +183,8 @@ describe('static_site_editor/pages/home', () => {
});
it('retries submitting changes when retry button is clicked', () => {
setupMutateMock();
findSubmitChangesError().vm.$emit('retry');
expect(mutateMock).toHaveBeenCalled();
......@@ -190,7 +209,11 @@ describe('static_site_editor/pages/home', () => {
const newContent = `new ${content}`;
beforeEach(() => {
mutateMock.mockResolvedValueOnce({ data: { submitContentChanges: savedContentMeta } });
mutateMock.mockResolvedValueOnce(hasSubmittedChangesMutationPayload).mockResolvedValueOnce({
data: {
submitContentChanges: savedContentMeta,
},
});
buildWrapper();
findEditArea().vm.$emit('submit', { content: newContent });
......@@ -198,8 +221,19 @@ describe('static_site_editor/pages/home', () => {
return wrapper.vm.$nextTick();
});
it('dispatches hasSubmittedChanges mutation', () => {
expect(mutateMock).toHaveBeenNthCalledWith(1, {
mutation: hasSubmittedChangesMutation,
variables: {
input: {
hasSubmittedChanges: true,
},
},
});
});
it('dispatches submitContentChanges mutation', () => {
expect(mutateMock).toHaveBeenCalledWith({
expect(mutateMock).toHaveBeenNthCalledWith(2, {
mutation: submitContentChangesMutation,
variables: {
input: {
......
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 { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
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';
let wrapper;
let router;
......@@ -15,14 +15,15 @@ describe('static_site_editor/pages/success', () => {
};
};
const buildWrapper = (data = {}) => {
const buildWrapper = (data = {}, appData = {}) => {
wrapper = shallowMount(Success, {
mocks: {
$router: router,
},
stubs: {
GlEmptyState,
GlButton,
GlEmptyState,
GlLoadingIcon,
},
propsData: {
mergeRequestsIllustrationPath,
......@@ -33,6 +34,8 @@ describe('static_site_editor/pages/success', () => {
appData: {
returnUrl,
sourcePath,
hasSubmittedChanges: true,
...appData,
},
...data,
};
......@@ -40,8 +43,9 @@ describe('static_site_editor/pages/success', () => {
});
};
const findEmptyState = () => wrapper.find(GlEmptyState);
const findReturnUrlButton = () => wrapper.find(GlButton);
const findEmptyState = () => wrapper.find(GlEmptyState);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
beforeEach(() => {
buildRouter();
......@@ -52,50 +56,75 @@ describe('static_site_editor/pages/success', () => {
wrapper = null;
});
it('renders empty state with a link to the created merge request', () => {
buildWrapper();
describe('when savedContentMeta is valid', () => {
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);
expect(findEmptyState().props()).toMatchObject({
primaryButtonText: 'View merge request',
primaryButtonLink: savedContentMeta.mergeRequest.url,
title: 'Your merge request has been created',
svgPath: mergeRequestsIllustrationPath,
it('displays merge request instructions in the empty state', () => {
buildWrapper();
expect(findEmptyState().text()).toContain(
'To see your changes live you will need to do the following things:',
);
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', () => {
buildWrapper();
expect(findEmptyState().text()).toContain(
'To see your changes live you will need to do the following things:',
);
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', () => {
buildWrapper();
expect(findReturnUrlButton().text()).toBe('Return to site');
expect(findReturnUrlButton().attributes().href).toBe(returnUrl);
});
it('displays return to site button', () => {
buildWrapper();
it('displays source path', () => {
buildWrapper();
expect(findReturnUrlButton().text()).toBe('Return to site');
expect(findReturnUrlButton().attributes().href).toBe(returnUrl);
expect(wrapper.text()).toContain(`Update ${sourcePath} file`);
});
});
it('displays source path', () => {
buildWrapper();
describe('when savedContentMeta is invalid', () => {
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', () => {
buildWrapper({ savedContentMeta: null });
it('displays helper info in the empty state', () => {
buildWrapper({ savedContentMeta: null });
expect(router.push).toHaveBeenCalledWith(HOME_ROUTE);
expect(wrapper.html()).toBe('');
expect(findEmptyState().text()).toContain(
'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