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