Commit 7b4a0066 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '216186-submit-changes-resolver' into 'master'

Use GraphQL to submit changes in the Static Site Editor

See merge request gitlab-org/gitlab!31779
parents ec5f86bd 805589b4
......@@ -2,8 +2,8 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import typeDefs from './typedefs.graphql';
import fileResolver from './resolvers/file';
import submitContentChangesResolver from './resolvers/submit_content_changes';
Vue.use(VueApollo);
......@@ -13,6 +13,9 @@ const createApolloProvider = appData => {
Project: {
file: fileResolver,
},
Mutation: {
submitContentChanges: submitContentChangesResolver,
},
},
{
typeDefs,
......
mutation submitContentChanges($input: SubmitContentChangesInput) {
submitContentChanges(input: $input) @client {
branch
commit
mergeRequest
}
}
......@@ -3,6 +3,7 @@ query appData {
isSupportedContent
project
sourcePath
username,
returnUrl
}
}
import submitContentChanges from '../../services/submit_content_changes';
import savedContentMetaQuery from '../queries/saved_content_meta.query.graphql';
const submitContentChangesResolver = (
_,
{ input: { project: projectId, username, sourcePath, content } },
{ cache },
) => {
return submitContentChanges({ projectId, username, sourcePath, content }).then(
savedContentMeta => {
cache.writeQuery({
query: savedContentMetaQuery,
data: {
savedContentMeta: {
__typename: 'SavedContentMeta',
...savedContentMeta,
},
},
});
},
);
};
export default submitContentChangesResolver;
......@@ -3,8 +3,15 @@ type File {
content: String!
}
extend type Project {
file(path: ID!): File
type SavedContentField {
label: String!
url: String!
}
type SavedContentMeta {
mergeRequest: SavedContentField!
commit: SavedContentField!
branch: SavedContentField!
}
type AppData {
......@@ -15,6 +22,22 @@ type AppData {
username: String!
}
type SubmitContentChangesInput {
project: String!
sourcePath: String!
content: String!
username: String!
}
extend type Project {
file(path: ID!): File
}
extend type Query {
appData: AppData!
savedContentMeta: SavedContentMeta
}
extend type Mutation {
submitContentChanges(input: SubmitContentChangesInput!): SavedContentMeta
}
<script>
import { mapState, mapActions } from 'vuex';
import SkeletonLoader from '../components/skeleton_loader.vue';
import EditArea from '../components/edit_area.vue';
import InvalidContentMessage from '../components/invalid_content_message.vue';
import SubmitChangesError from '../components/submit_changes_error.vue';
import { SUCCESS_ROUTE } from '../router/constants';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
import sourceContentQuery from '../graphql/queries/source_content.query.graphql';
import submitContentChangesMutation from '../graphql/mutations/submit_content_changes.mutation.graphql';
import createFlash from '~/flash';
import { LOAD_CONTENT_ERROR } from '../constants';
import { SUCCESS_ROUTE } from '../router/constants';
export default {
components: {
......@@ -44,8 +44,14 @@ export default {
},
},
},
data() {
return {
content: null,
submitChangesError: null,
isSavingChanges: false,
};
},
computed: {
...mapState(['isSavingChanges', 'submitChangesError']),
isLoadingContent() {
return this.$apollo.queries.sourceContent.loading;
},
......@@ -54,11 +60,37 @@ export default {
},
},
methods: {
...mapActions(['setContent', 'submitChanges', 'dismissSubmitChangesError']),
onDismissError() {
this.submitChangesError = null;
},
onSubmit({ content }) {
this.setContent(content);
this.content = content;
this.submitChanges();
},
submitChanges() {
this.isSavingChanges = true;
return this.submitChanges().then(() => this.$router.push(SUCCESS_ROUTE));
this.$apollo
.mutate({
mutation: submitContentChangesMutation,
variables: {
input: {
project: this.appData.project,
username: this.appData.username,
sourcePath: this.appData.sourcePath,
content: this.content,
},
},
})
.then(() => {
this.$router.push(SUCCESS_ROUTE);
})
.catch(e => {
this.submitChangesError = e.message;
})
.finally(() => {
this.isSavingChanges = false;
});
},
},
};
......@@ -71,7 +103,7 @@ export default {
v-if="submitChangesError"
:error="submitChangesError"
@retry="submitChanges"
@dismiss="dismissSubmitChangesError"
@dismiss="onDismissError"
/>
<edit-area
v-if="isContentLoaded"
......
<script>
import { mapState } from 'vuex';
import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
import SavedChangesMessage from '../components/saved_changes_message.vue';
import { HOME_ROUTE } from '../router/constants';
......@@ -7,8 +8,13 @@ export default {
components: {
SavedChangesMessage,
},
computed: {
...mapState(['savedContentMeta', 'returnUrl']),
apollo: {
savedContentMeta: {
query: savedContentMetaQuery,
},
appData: {
query: appDataQuery,
},
},
created() {
if (!this.savedContentMeta) {
......@@ -23,7 +29,7 @@ export default {
:branch="savedContentMeta.branch"
:commit="savedContentMeta.commit"
:merge-request="savedContentMeta.mergeRequest"
:return-url="returnUrl"
:return-url="appData.returnUrl"
/>
</div>
</template>
import savedContentMetaQuery from '~/static_site_editor/graphql/queries/saved_content_meta.query.graphql';
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
import submitContentChangesResolver from '~/static_site_editor/graphql/resolvers/submit_content_changes';
import {
projectId as project,
sourcePath,
username,
sourceContent as content,
savedContentMeta,
} from '../../mock_data';
jest.mock('~/static_site_editor/services/submit_content_changes', () => jest.fn());
describe('static_site_editor/graphql/resolvers/submit_content_changes', () => {
it('writes savedContentMeta query with the data returned by the submitContentChanges service', () => {
const cache = { writeQuery: jest.fn() };
submitContentChanges.mockResolvedValueOnce(savedContentMeta);
return submitContentChangesResolver(
{},
{ input: { path: sourcePath, project, sourcePath, content, username } },
{ cache },
).then(() => {
expect(cache.writeQuery).toHaveBeenCalledWith({
query: savedContentMetaQuery,
data: {
savedContentMeta: {
__typename: 'SavedContentMeta',
...savedContentMeta,
},
},
});
});
});
});
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import createState from '~/static_site_editor/store/state';
import { SUCCESS_ROUTE } from '~/static_site_editor/router/constants';
import Home from '~/static_site_editor/pages/home.vue';
import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
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 { SUCCESS_ROUTE } from '~/static_site_editor/router/constants';
import {
projectId as project,
returnUrl,
sourceContent as content,
sourceContentTitle as title,
sourcePath,
username,
savedContentMeta,
submitChangesError,
} from '../mock_data';
......@@ -24,32 +28,11 @@ describe('static_site_editor/pages/home', () => {
let store;
let $apollo;
let $router;
let setContentActionMock;
let submitChangesActionMock;
let dismissSubmitChangesErrorActionMock;
const buildStore = ({ initialState, getters } = {}) => {
setContentActionMock = jest.fn();
submitChangesActionMock = jest.fn();
dismissSubmitChangesErrorActionMock = jest.fn();
store = new Vuex.Store({
state: createState({
...initialState,
}),
getters: {
contentChanged: () => false,
...getters,
},
actions: {
setContent: setContentActionMock,
submitChanges: submitChangesActionMock,
dismissSubmitChangesError: dismissSubmitChangesErrorActionMock,
},
});
};
let mutateMock;
const buildApollo = (queries = {}) => {
mutateMock = jest.fn();
$apollo = {
queries: {
sourceContent: {
......@@ -57,6 +40,7 @@ describe('static_site_editor/pages/home', () => {
},
...queries,
},
mutate: mutateMock,
};
};
......@@ -76,7 +60,8 @@ describe('static_site_editor/pages/home', () => {
},
data() {
return {
appData: { isSupportedContent: true, returnUrl },
appData: { isSupportedContent: true, returnUrl, project, username, sourcePath },
sourceContent: { title, content },
...data,
};
},
......@@ -91,7 +76,6 @@ describe('static_site_editor/pages/home', () => {
beforeEach(() => {
buildApollo();
buildRouter();
buildStore();
});
afterEach(() => {
......@@ -102,8 +86,7 @@ describe('static_site_editor/pages/home', () => {
describe('when content is loaded', () => {
beforeEach(() => {
buildStore({ initialState: { isSavingChanges: true } });
buildWrapper({ sourceContent: { title, content } });
buildWrapper();
});
it('renders edit area', () => {
......@@ -115,7 +98,7 @@ describe('static_site_editor/pages/home', () => {
title,
content,
returnUrl,
savingChanges: true,
savingChanges: false,
});
});
});
......@@ -148,30 +131,44 @@ describe('static_site_editor/pages/home', () => {
expect(findSkeletonLoader().exists()).toBe(false);
});
describe('when submitting changes fail', () => {
it('displays invalid content message when content is not supported', () => {
buildWrapper({ appData: { isSupportedContent: false } });
expect(findInvalidContentMessage().exists()).toBe(true);
});
it('does not display invalid content message when content is supported', () => {
buildWrapper({ appData: { isSupportedContent: true } });
expect(findInvalidContentMessage().exists()).toBe(false);
});
describe('when submitting changes fails', () => {
beforeEach(() => {
buildStore({
initialState: {
submitChangesError,
},
});
mutateMock.mockRejectedValue(new Error(submitChangesError));
buildWrapper();
findEditArea().vm.$emit('submit', { content });
return wrapper.vm.$nextTick();
});
it('displays submit changes error message', () => {
expect(findSubmitChangesError().exists()).toBe(true);
});
it('dispatches submitChanges action when error message emits retry event', () => {
it('retries submitting changes when retry button is clicked', () => {
findSubmitChangesError().vm.$emit('retry');
expect(submitChangesActionMock).toHaveBeenCalled();
expect(mutateMock).toHaveBeenCalled();
});
it('dispatches dismissSubmitChangesError action when error message emits dismiss event', () => {
it('hides submit changes error message when dismiss button is clicked', () => {
findSubmitChangesError().vm.$emit('dismiss');
expect(dismissSubmitChangesErrorActionMock).toHaveBeenCalled();
return wrapper.vm.$nextTick().then(() => {
expect(findSubmitChangesError().exists()).toBe(false);
});
});
});
......@@ -181,34 +178,34 @@ describe('static_site_editor/pages/home', () => {
expect(findSubmitChangesError().exists()).toBe(false);
});
it('displays invalid content message when content is not supported', () => {
buildWrapper({ appData: { isSupportedContent: false } });
expect(findInvalidContentMessage().exists()).toBe(true);
});
describe('when edit area emits submit event', () => {
describe('when submitting changes succeeds', () => {
const newContent = `new ${content}`;
beforeEach(() => {
submitChangesActionMock.mockResolvedValueOnce();
mutateMock.mockResolvedValueOnce({ data: { submitContentChanges: savedContentMeta } });
buildWrapper({ sourceContent: { title, content } });
buildWrapper();
findEditArea().vm.$emit('submit', { content: newContent });
});
it('dispatches setContent property', () => {
expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), newContent, undefined);
return wrapper.vm.$nextTick();
});
it('dispatches submitChanges action', () => {
expect(submitChangesActionMock).toHaveBeenCalled();
it('dispatches submitContentChanges mutation', () => {
expect(mutateMock).toHaveBeenCalledWith({
mutation: submitContentChangesMutation,
variables: {
input: {
content: newContent,
project,
sourcePath,
username,
},
},
});
});
it('pushes success route when submitting changes succeeds', () => {
return wrapper.vm.$nextTick().then(() => {
expect($router.push).toHaveBeenCalledWith(SUCCESS_ROUTE);
});
it('transitions to the SUCCESS route', () => {
expect($router.push).toHaveBeenCalledWith(SUCCESS_ROUTE);
});
});
});
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import createState from '~/static_site_editor/store/state';
import Success from '~/static_site_editor/pages/success.vue';
import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
import { savedContentMeta, returnUrl } from '../mock_data';
......@@ -21,23 +20,22 @@ describe('static_site_editor/pages/success', () => {
};
};
const buildStore = (initialState = {}) => {
store = new Vuex.Store({
state: createState({
savedContentMeta,
returnUrl,
...initialState,
}),
});
};
const buildWrapper = () => {
const buildWrapper = (data = {}) => {
wrapper = shallowMount(Success, {
localVue,
store,
mocks: {
$router: router,
},
data() {
return {
savedContentMeta,
appData: {
returnUrl,
},
...data,
};
},
});
};
......@@ -45,7 +43,6 @@ describe('static_site_editor/pages/success', () => {
beforeEach(() => {
buildRouter();
buildStore();
});
afterEach(() => {
......@@ -74,8 +71,7 @@ describe('static_site_editor/pages/success', () => {
});
it('redirects to the HOME route when content has not been submitted', () => {
buildStore({ savedContentMeta: null });
buildWrapper();
buildWrapper({ savedContentMeta: null });
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