Commit 75365338 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch...

Merge branch '35073-refactor-design_management-pages-index-vue-to-use-apollomutation-component' into 'master'

Refactor design_management/pages/index.vue to use ApolloMutation component

See merge request gitlab-org/gitlab!21022
parents df049de2 9ad18925
<script> <script>
import { GlLoadingIcon, GlEmptyState, GlButton } from '@gitlab/ui'; import { GlLoadingIcon, GlEmptyState, GlButton } from '@gitlab/ui';
import _ from 'underscore';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import UploadButton from '../components/upload/button.vue'; import UploadButton from '../components/upload/button.vue';
...@@ -14,6 +13,7 @@ import projectQuery from '../graphql/queries/project.query.graphql'; ...@@ -14,6 +13,7 @@ import projectQuery from '../graphql/queries/project.query.graphql';
import allDesignsMixin from '../mixins/all_designs'; import allDesignsMixin from '../mixins/all_designs';
import { UPLOAD_DESIGN_ERROR } from '../utils/error_messages'; import { UPLOAD_DESIGN_ERROR } from '../utils/error_messages';
import { updateStoreAfterUploadDesign } from '../utils/cache_update'; import { updateStoreAfterUploadDesign } from '../utils/cache_update';
import { designUploadOptimisticResponse } from '../utils/design_management_utils';
const MAXIMUM_FILE_UPLOAD_LIMIT = 10; const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
...@@ -85,8 +85,15 @@ export default { ...@@ -85,8 +85,15 @@ export default {
}, },
}, },
methods: { methods: {
onUploadDesign(files) { resetFilesToBeSaved() {
if (!this.canCreateDesign) return null; this.filesToBeSaved = [];
},
/**
* Determine if a design upload is valid, given [files]
* @param {Array<File>} files
*/
isValidDesignUpload(files) {
if (!this.canCreateDesign) return false;
if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) { if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) {
createFlash( createFlash(
...@@ -100,77 +107,49 @@ export default { ...@@ -100,77 +107,49 @@ export default {
), ),
); );
return null; return false;
} }
return true;
},
onUploadDesign(files) {
// convert to Array so that we have Array methods (.map, .some, etc.)
this.filesToBeSaved = Array.from(files); this.filesToBeSaved = Array.from(files);
const optimisticResponse = this.filesToBeSaved.map(file => ({ if (!this.isValidDesignUpload(this.filesToBeSaved)) return null;
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings const mutationPayload = {
__typename: 'Design', optimisticResponse: designUploadOptimisticResponse(this.filesToBeSaved),
id: -_.uniqueId(), variables: {
image: '', files: this.filesToBeSaved,
filename: file.name, projectPath: this.projectPath,
fullPath: '', iid: this.issueIid,
notesCount: 0,
event: 'NONE',
diffRefs: {
__typename: 'DiffRefs',
baseSha: '',
startSha: '',
headSha: '',
}, },
discussions: { context: {
__typename: 'DesignDiscussion', hasUpload: true,
edges: [],
}, },
versions: { mutation: uploadDesignMutation,
__typename: 'DesignVersionConnection', update: this.afterUploadDesign,
edges: { };
__typename: 'DesignVersionEdge',
node: {
__typename: 'DesignVersion',
id: -_.uniqueId(),
sha: -_.uniqueId(),
},
},
},
}));
return this.$apollo return this.$apollo
.mutate({ .mutate(mutationPayload)
mutation: uploadDesignMutation, .then(() => this.onUploadDesignDone())
variables: { .catch(() => this.onUploadDesignError());
files, },
projectPath: this.projectPath, afterUploadDesign(
iid: this.issueIid, store,
}, {
context: { data: { designManagementUpload },
hasUpload: true, },
}, ) {
update: (store, { data: { designManagementUpload } }) => { updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody);
updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody); },
}, onUploadDesignDone() {
optimisticResponse: { this.resetFilesToBeSaved();
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 this.$router.push({ name: 'designs' });
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings },
__typename: 'Mutation', onUploadDesignError() {
designManagementUpload: { this.resetFilesToBeSaved();
__typename: 'DesignManagementUploadPayload', createFlash(UPLOAD_DESIGN_ERROR);
designs: optimisticResponse,
},
},
})
.then(() => {
this.$router.push({ name: 'designs' });
})
.catch(e => {
createFlash(UPLOAD_DESIGN_ERROR);
throw e;
})
.finally(() => {
this.filesToBeSaved = [];
});
}, },
changeSelectedDesigns(filename) { changeSelectedDesigns(filename) {
if (this.isDesignSelected(filename)) { if (this.isDesignSelected(filename)) {
......
import { uniqueId } from 'underscore';
/** /**
* Returns formatted array that doesn't contain * Returns formatted array that doesn't contain
* `edges`->`node` nesting * `edges`->`node` nesting
...@@ -35,3 +37,52 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1]; ...@@ -35,3 +37,52 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1];
export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1]; export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
export const extractDesign = data => data.project.issue.designCollection.designs.edges[0].node; export const extractDesign = data => data.project.issue.designCollection.designs.edges[0].node;
/**
* Generates optimistic response for a design upload mutation
* @param {Array<File>} files
*/
export const designUploadOptimisticResponse = files => {
const designs = files.map(file => ({
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
__typename: 'Design',
id: -uniqueId(),
image: '',
filename: file.name,
fullPath: '',
notesCount: 0,
event: 'NONE',
diffRefs: {
__typename: 'DiffRefs',
baseSha: '',
startSha: '',
headSha: '',
},
discussions: {
__typename: 'DesignDiscussion',
edges: [],
},
versions: {
__typename: 'DesignVersionConnection',
edges: {
__typename: 'DesignVersionEdge',
node: {
__typename: 'DesignVersion',
id: -uniqueId(),
sha: -uniqueId(),
},
},
},
}));
return {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
__typename: 'Mutation',
designManagementUpload: {
__typename: 'DesignManagementUploadPayload',
designs,
},
};
};
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import { GlEmptyState } from '@gitlab/ui';
import Index from 'ee/design_management/pages/index.vue'; import Index from 'ee/design_management/pages/index.vue';
import uploadDesignQuery from 'ee/design_management/graphql/mutations/uploadDesign.mutation.graphql'; import uploadDesignQuery from 'ee/design_management/graphql/mutations/uploadDesign.mutation.graphql';
import DesignDestroyer from 'ee/design_management/components/design_destroyer.vue'; import DesignDestroyer from 'ee/design_management/components/design_destroyer.vue';
import UploadButton from 'ee/design_management/components/upload/button.vue';
import DeleteButton from 'ee/design_management/components/delete_button.vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -55,14 +60,16 @@ describe('Design management index page', () => { ...@@ -55,14 +60,16 @@ describe('Design management index page', () => {
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox'); const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
const findSelectAllButton = () => wrapper.find('.js-select-all'); const findSelectAllButton = () => wrapper.find('.js-select-all');
const findDeleteButton = () => wrapper.find('deletebutton-stub');
const findToolbar = () => wrapper.find('.qa-selector-toolbar'); const findToolbar = () => wrapper.find('.qa-selector-toolbar');
const findDeleteButton = () => wrapper.find(DeleteButton);
const findUploadButton = () => wrapper.find(UploadButton);
function createComponent({ function createComponent({
loading = false, loading = false,
designs = [], designs = [],
allVersions = [], allVersions = [],
createDesign = true, createDesign = true,
stubs = {},
} = {}) { } = {}) {
mutate = jest.fn(() => Promise.resolve()); mutate = jest.fn(() => Promise.resolve());
const $apollo = { const $apollo = {
...@@ -82,7 +89,7 @@ describe('Design management index page', () => { ...@@ -82,7 +89,7 @@ describe('Design management index page', () => {
mocks: { $apollo }, mocks: { $apollo },
localVue, localVue,
router, router,
stubs: { DesignDestroyer }, stubs: { DesignDestroyer, ApolloMutation, ...stubs },
}); });
wrapper.setData({ wrapper.setData({
...@@ -155,76 +162,67 @@ describe('Design management index page', () => { ...@@ -155,76 +162,67 @@ describe('Design management index page', () => {
}); });
}); });
describe('onUploadDesign', () => { describe('uploading designs', () => {
it('calls apollo mutate', () => { it('calls mutation on upload', () => {
createComponent(); createComponent({ stubs: { GlEmptyState } });
return wrapper.vm const mutationVariables = {
.onUploadDesign([ update: expect.anything(),
{ context: {
name: 'test', hasUpload: true,
}, },
]) mutation: uploadDesignQuery,
.then(() => { variables: {
expect(mutate).toHaveBeenCalledWith({ files: [{ name: 'test' }],
context: { projectPath: '',
hasUpload: true, iid: '1',
}, },
mutation: uploadDesignQuery, optimisticResponse: {
variables: { __typename: 'Mutation',
files: [{ name: 'test' }], designManagementUpload: {
projectPath: '', __typename: 'DesignManagementUploadPayload',
iid: '1', designs: [
}, {
update: expect.anything(), __typename: 'Design',
optimisticResponse: { id: expect.anything(),
__typename: 'Mutation', image: '',
designManagementUpload: { filename: 'test',
__typename: 'DesignManagementUploadPayload', fullPath: '',
designs: [ event: 'NONE',
{ notesCount: 0,
__typename: 'Design', diffRefs: {
id: expect.anything(), __typename: 'DiffRefs',
image: '', baseSha: '',
filename: 'test', startSha: '',
fullPath: '', headSha: '',
event: 'NONE', },
notesCount: 0, discussions: {
diffRefs: { __typename: 'DesignDiscussion',
__typename: 'DiffRefs', edges: [],
baseSha: '', },
startSha: '', versions: {
headSha: '', __typename: 'DesignVersionConnection',
}, edges: {
discussions: { __typename: 'DesignVersionEdge',
__typename: 'DesignDiscussion', node: {
edges: [], __typename: 'DesignVersion',
}, id: expect.anything(),
versions: { sha: expect.anything(),
__typename: 'DesignVersionConnection',
edges: {
__typename: 'DesignVersionEdge',
node: {
__typename: 'DesignVersion',
id: expect.anything(),
sha: expect.anything(),
},
},
}, },
}, },
], },
}, },
}, ],
}); },
}); },
}); };
it('does not call apollo mutate if createDesign is false', () => {
createComponent({ createDesign: false });
wrapper.vm.onUploadDesign([]);
expect(mutate).not.toHaveBeenCalled(); return wrapper.vm.$nextTick().then(() => {
findUploadButton().vm.$emit('upload', [{ name: 'test' }]);
expect(mutate).toHaveBeenCalledWith(mutationVariables);
expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
expect(wrapper.vm.isSaving).toBeTruthy();
});
}); });
it('sets isSaving', () => { it('sets isSaving', () => {
...@@ -243,6 +241,40 @@ describe('Design management index page', () => { ...@@ -243,6 +241,40 @@ describe('Design management index page', () => {
}); });
}); });
it('updates state appropriately after upload complete', () => {
createComponent({ stubs: { GlEmptyState } });
wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
wrapper.vm.onUploadDesignDone();
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.filesToBeSaved).toEqual([]);
expect(wrapper.vm.isSaving).toBeFalsy();
expect(wrapper.vm.$router.currentRoute.path).toEqual('/designs');
});
});
it('updates state appropriately after upload error', () => {
createComponent({ stubs: { GlEmptyState } });
wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
wrapper.vm.onUploadDesignError();
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.filesToBeSaved).toEqual([]);
expect(wrapper.vm.isSaving).toBeFalsy();
expect(createFlash).toHaveBeenCalled();
createFlash.mockReset();
});
});
it('does not call mutation if createDesign is false', () => {
createComponent({ createDesign: false });
wrapper.vm.onUploadDesign([]);
expect(mutate).not.toHaveBeenCalled();
});
describe('upload count limit', () => { describe('upload count limit', () => {
const MAXIMUM_FILE_UPLOAD_LIMIT = 10; const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
......
import underscore from 'underscore';
import { import {
extractCurrentDiscussion, extractCurrentDiscussion,
extractDiscussions, extractDiscussions,
findVersionId, findVersionId,
designUploadOptimisticResponse,
} from 'ee/design_management/utils/design_management_utils'; } from 'ee/design_management/utils/design_management_utils';
describe('extractCurrentDiscussion', () => { describe('extractCurrentDiscussion', () => {
...@@ -68,3 +70,36 @@ describe('version parser', () => { ...@@ -68,3 +70,36 @@ describe('version parser', () => {
expect(findVersionId(testInvalidVersionString)).toBeUndefined(); expect(findVersionId(testInvalidVersionString)).toBeUndefined();
}); });
}); });
describe('optimistic responses', () => {
it('correctly generated for design upload', () => {
jest.spyOn(underscore, 'uniqueId').mockImplementation(() => 1);
const expectedResponse = {
__typename: 'Mutation',
designManagementUpload: {
__typename: 'DesignManagementUploadPayload',
designs: [
{
__typename: 'Design',
id: -1,
image: '',
filename: 'test',
fullPath: '',
notesCount: 0,
event: 'NONE',
diffRefs: { __typename: 'DiffRefs', baseSha: '', startSha: '', headSha: '' },
discussions: { __typename: 'DesignDiscussion', edges: [] },
versions: {
__typename: 'DesignVersionConnection',
edges: {
__typename: 'DesignVersionEdge',
node: { __typename: 'DesignVersion', id: -1, sha: -1 },
},
},
},
],
},
};
expect(designUploadOptimisticResponse([{ name: 'test' }])).toEqual(expectedResponse);
});
});
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