Commit f0c496e9 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'fix-infinite-spinner-design-management' into 'master'

Resolve "Infinite spinner when trying to open a non-existent design"

See merge request gitlab-org/gitlab!30263
parents 10162024 1de75626
...@@ -31,7 +31,7 @@ import { ...@@ -31,7 +31,7 @@ import {
ADD_IMAGE_DIFF_NOTE_ERROR, ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR, UPDATE_IMAGE_DIFF_NOTE_ERROR,
DESIGN_NOT_FOUND_ERROR, DESIGN_NOT_FOUND_ERROR,
DESIGN_NOT_EXIST_ERROR, DESIGN_VERSION_NOT_EXIST_ERROR,
designDeletionError, designDeletionError,
} from '../../utils/error_messages'; } from '../../utils/error_messages';
import { DESIGNS_ROUTE_NAME } from '../../router/constants'; import { DESIGNS_ROUTE_NAME } from '../../router/constants';
...@@ -84,18 +84,8 @@ export default { ...@@ -84,18 +84,8 @@ export default {
return this.designVariables; return this.designVariables;
}, },
update: data => extractDesign(data), update: data => extractDesign(data),
result({ data, loading }) { result(res) {
// On the initial load with cache-and-network policy data is undefined while loading is true this.onDesignQueryResult(res);
// To prevent throwing an error, we don't perform any logic until loading is false
if (loading) {
return;
}
if (!data) {
this.onQueryError(DESIGN_NOT_FOUND_ERROR);
}
if (this.$route.query.version && !this.hasValidVersion) {
this.onQueryError(DESIGN_NOT_EXIST_ERROR);
}
}, },
error() { error() {
this.onQueryError(DESIGN_NOT_FOUND_ERROR); this.onQueryError(DESIGN_NOT_FOUND_ERROR);
...@@ -215,6 +205,19 @@ export default { ...@@ -215,6 +205,19 @@ export default {
return this.$apollo.mutate(mutationPayload).catch(e => this.onUpdateImageDiffNoteError(e)); return this.$apollo.mutate(mutationPayload).catch(e => this.onUpdateImageDiffNoteError(e));
}, },
onDesignQueryResult({ data, loading }) {
// On the initial load with cache-and-network policy data is undefined while loading is true
// To prevent throwing an error, we don't perform any logic until loading is false
if (loading) {
return;
}
if (!data || !extractDesign(data)) {
this.onQueryError(DESIGN_NOT_FOUND_ERROR);
} else if (this.$route.query.version && !this.hasValidVersion) {
this.onQueryError(DESIGN_VERSION_NOT_EXIST_ERROR);
}
},
onQueryError(message) { onQueryError(message) {
// because we redirect user to /designs (the issue page), // because we redirect user to /designs (the issue page),
// we want to create these flashes on the issue page // we want to create these flashes on the issue page
......
...@@ -39,7 +39,9 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1]; ...@@ -39,7 +39,9 @@ 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 extractDesigns = data => data.project.issue.designCollection.designs.edges;
export const extractDesign = data => (extractDesigns(data) || [])[0]?.node;
/** /**
* Generates optimistic response for a design upload mutation * Generates optimistic response for a design upload mutation
......
...@@ -20,9 +20,9 @@ export const UPLOAD_DESIGN_INVALID_FILETYPE_ERROR = __( ...@@ -20,9 +20,9 @@ export const UPLOAD_DESIGN_INVALID_FILETYPE_ERROR = __(
'Could not upload your designs as one or more files uploaded are not supported.', 'Could not upload your designs as one or more files uploaded are not supported.',
); );
export const DESIGN_NOT_FOUND_ERROR = __('Could not find design'); export const DESIGN_NOT_FOUND_ERROR = __('Could not find design.');
export const DESIGN_NOT_EXIST_ERROR = __('Requested design version does not exist'); export const DESIGN_VERSION_NOT_EXIST_ERROR = __('Requested design version does not exist.');
const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped.'); const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped.');
......
---
title: Fix infinte loading spinner when visiting non-existent design
merge_request: 30263
author:
type: fixed
export default [
{
node: {
id: 'gid://gitlab/DesignManagement::Version/1',
sha: 'b389071a06c153509e11da1f582005b316667001',
},
},
];
import design from './design';
export default {
project: {
issue: {
designCollection: {
designs: {
edges: [
{
node: design,
},
],
},
},
},
},
};
export default {
project: {
issue: {
designCollection: {
designs: {
edges: [],
},
},
},
},
};
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo'; import { ApolloMutation } from 'vue-apollo';
import createFlash from '~/flash';
import DesignIndex from 'ee/design_management/pages/design/index.vue'; import DesignIndex from 'ee/design_management/pages/design/index.vue';
import DesignDiscussion from 'ee/design_management/components/design_notes/design_discussion.vue'; import DesignDiscussion from 'ee/design_management/components/design_notes/design_discussion.vue';
import DesignReplyForm from 'ee/design_management/components/design_notes/design_reply_form.vue'; import DesignReplyForm from 'ee/design_management/components/design_notes/design_reply_form.vue';
import Participants from '~/sidebar/components/participants/participants.vue'; import Participants from '~/sidebar/components/participants/participants.vue';
import createImageDiffNoteMutation from 'ee/design_management/graphql/mutations/createImageDiffNote.mutation.graphql'; import createImageDiffNoteMutation from 'ee/design_management/graphql/mutations/createImageDiffNote.mutation.graphql';
import design from '../../mock_data/design'; import design from '../../mock_data/design';
import mockResponseWithDesigns from '../../mock_data/designs';
import mockResponseNoDesigns from '../../mock_data/no_designs';
import mockAllVersions from '../../mock_data/all_versions';
import {
DESIGN_NOT_FOUND_ERROR,
DESIGN_VERSION_NOT_EXIST_ERROR,
} from 'ee/design_management/utils/error_messages';
import { DESIGNS_ROUTE_NAME } from 'ee/design_management/router/constants';
jest.mock('~/flash');
jest.mock('mousetrap', () => ({ jest.mock('mousetrap', () => ({
bind: jest.fn(), bind: jest.fn(),
unbind: jest.fn(), unbind: jest.fn(),
...@@ -41,13 +51,14 @@ describe('Design management design index page', () => { ...@@ -41,13 +51,14 @@ describe('Design management design index page', () => {
}, },
}, },
}; };
const mutate = jest.fn(() => Promise.resolve()); const mutate = jest.fn().mockResolvedValue();
const routerPush = jest.fn();
const findDiscussions = () => wrapper.findAll(DesignDiscussion); const findDiscussions = () => wrapper.findAll(DesignDiscussion);
const findDiscussionForm = () => wrapper.find(DesignReplyForm); const findDiscussionForm = () => wrapper.find(DesignReplyForm);
const findParticipants = () => wrapper.find(Participants); const findParticipants = () => wrapper.find(Participants);
function createComponent(loading = false) { function createComponent(loading = false, { routeQuery = {} } = {}) {
const $apollo = { const $apollo = {
queries: { queries: {
design: { design: {
...@@ -57,9 +68,17 @@ describe('Design management design index page', () => { ...@@ -57,9 +68,17 @@ describe('Design management design index page', () => {
mutate, mutate,
}; };
const $router = {
push: routerPush,
};
const $route = {
query: routeQuery,
};
wrapper = shallowMount(DesignIndex, { wrapper = shallowMount(DesignIndex, {
propsData: { id: '1' }, propsData: { id: '1' },
mocks: { $apollo }, mocks: { $apollo, $router, $route },
stubs: { stubs: {
ApolloMutation, ApolloMutation,
}, },
...@@ -75,6 +94,12 @@ describe('Design management design index page', () => { ...@@ -75,6 +94,12 @@ describe('Design management design index page', () => {
wrapper.vm.$apollo.queries.design.loading = false; wrapper.vm.$apollo.queries.design.loading = false;
} }
function setDesignData() {
wrapper.setData({
design,
});
}
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -89,10 +114,7 @@ describe('Design management design index page', () => { ...@@ -89,10 +114,7 @@ describe('Design management design index page', () => {
it('renders design index', () => { it('renders design index', () => {
setDesign(); setDesign();
setDesignData();
wrapper.setData({
design,
});
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
...@@ -102,10 +124,7 @@ describe('Design management design index page', () => { ...@@ -102,10 +124,7 @@ describe('Design management design index page', () => {
it('renders participants', () => { it('renders participants', () => {
setDesign(); setDesign();
setDesignData();
wrapper.setData({
design,
});
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findParticipants().exists()).toBe(true); expect(findParticipants().exists()).toBe(true);
...@@ -142,10 +161,7 @@ describe('Design management design index page', () => { ...@@ -142,10 +161,7 @@ describe('Design management design index page', () => {
describe('when has discussions', () => { describe('when has discussions', () => {
beforeEach(() => { beforeEach(() => {
setDesign(); setDesign();
setDesignData();
wrapper.setData({
design,
});
}); });
it('renders correct amount of discussions', () => { it('renders correct amount of discussions', () => {
...@@ -245,4 +261,38 @@ describe('Design management design index page', () => { ...@@ -245,4 +261,38 @@ describe('Design management design index page', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
}); });
describe('onDesignQueryResult', () => {
describe('with no designs', () => {
it('redirects to /designs', () => {
createComponent(true);
wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false });
return wrapper.vm.$nextTick().then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(DESIGN_NOT_FOUND_ERROR);
expect(routerPush).toHaveBeenCalledTimes(1);
expect(routerPush).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
});
});
});
describe('when no design exists for given version', () => {
it('redirects to /designs', () => {
// attempt to query for a version of the design that doesn't exist
createComponent(true, { routeQuery: { version: '999' } });
wrapper.setData({
allVersions: mockAllVersions,
});
wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false });
return wrapper.vm.$nextTick().then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
expect(createFlash).toHaveBeenCalledWith(DESIGN_VERSION_NOT_EXIST_ERROR);
expect(routerPush).toHaveBeenCalledTimes(1);
expect(routerPush).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
});
});
});
});
}); });
...@@ -5,7 +5,11 @@ import { ...@@ -5,7 +5,11 @@ import {
designUploadOptimisticResponse, designUploadOptimisticResponse,
updateImageDiffNoteOptimisticResponse, updateImageDiffNoteOptimisticResponse,
isValidDesignFile, isValidDesignFile,
extractDesign,
} from 'ee/design_management/utils/design_management_utils'; } from 'ee/design_management/utils/design_management_utils';
import mockResponseNoDesigns from '../mock_data/no_designs';
import mockResponseWithDesigns from '../mock_data/designs';
import mockDesign from '../mock_data/design';
jest.mock('lodash/uniqueId', () => () => 1); jest.mock('lodash/uniqueId', () => () => 1);
...@@ -152,7 +156,21 @@ describe('isValidDesignFile', () => { ...@@ -152,7 +156,21 @@ describe('isValidDesignFile', () => {
${'video/mpeg'} | ${false} ${'video/mpeg'} | ${false}
${'audio/midi'} | ${false} ${'audio/midi'} | ${false}
${'application/octet-stream'} | ${false} ${'application/octet-stream'} | ${false}
`('returns $valid for file type $mimetype', ({ mimetype, isValid }) => { `('returns $isValid for file type $mimetype', ({ mimetype, isValid }) => {
expect(isValidDesignFile({ type: mimetype })).toBe(isValid); expect(isValidDesignFile({ type: mimetype })).toBe(isValid);
}); });
}); });
describe('extractDesign', () => {
describe('with no designs', () => {
it('returns undefined', () => {
expect(extractDesign(mockResponseNoDesigns)).toBeUndefined();
});
});
describe('with designs', () => {
it('returns the first design available', () => {
expect(extractDesign(mockResponseWithDesigns)).toEqual(mockDesign);
});
});
});
...@@ -5992,7 +5992,7 @@ msgstr "" ...@@ -5992,7 +5992,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}." msgid "Could not delete chat nickname %{chat_name}."
msgstr "" msgstr ""
msgid "Could not find design" msgid "Could not find design."
msgstr "" msgstr ""
msgid "Could not remove the trigger." msgid "Could not remove the trigger."
...@@ -17458,7 +17458,7 @@ msgstr "" ...@@ -17458,7 +17458,7 @@ msgstr ""
msgid "Requested %{time_ago}" msgid "Requested %{time_ago}"
msgstr "" msgstr ""
msgid "Requested design version does not exist" msgid "Requested design version does not exist."
msgstr "" msgstr ""
msgid "Requested states are invalid" msgid "Requested states are invalid"
......
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