Commit 404eab8b authored by Natalia Tepluhina's avatar Natalia Tepluhina Committed by Kushal Pandya

Fixed designs to display correct version

- persisted version as router query parameter;
- implemented errors on incorrect version;
- refactored mixins to separate versions from project
parent ba46f030
......@@ -2,6 +2,7 @@
import { s__ } from '~/locale';
import createFlash from '~/flash';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import allVersionsMixin from '../../mixins/all_versions';
import createNoteMutation from '../../graphql/mutations/createNote.mutation.graphql';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import DesignNote from './design_note.vue';
......@@ -14,6 +15,7 @@ export default {
ReplyPlaceholder,
DesignReplyForm,
},
mixins: [allVersionsMixin],
props: {
discussion: {
type: Object,
......@@ -67,6 +69,7 @@ export default {
query: getDesignQuery,
variables: {
id: this.designId,
version: this.designsVersion,
},
});
const currentDiscussion = extractCurrentDiscussion(
......
......@@ -42,7 +42,11 @@ export default {
<template>
<router-link
:to="{ name: 'design', params: { id: name } }"
:to="{
name: 'design',
params: { id: name },
query: $route.query,
}"
class="card cursor-pointer text-plain js-design-list-item design-list-item"
>
<div class="card-body p-0 d-flex align-items-center overflow-hidden">
......
......@@ -47,7 +47,10 @@ export default {
<template>
<header class="d-flex w-100 p-2 bg-white align-items-center js-design-header">
<router-link
:to="{ name: 'designs' }"
:to="{
name: 'designs',
query: $route.query,
}"
:aria-label="s__('DesignManagement|Go back to designs')"
class="mr-3 text-plain"
>
......
......@@ -24,7 +24,11 @@ export default {
designLink() {
if (!this.design) return {};
return { name: 'design', params: { id: this.design.filename } };
return {
name: 'design',
params: { id: this.design.filename },
query: this.$route.query,
};
},
},
};
......
<script>
import UploadButton from './button.vue';
import allVersionsMixin from '../../mixins/all_versions';
import DesignVersionDropdown from './design_version_dropdown.vue';
export default {
......@@ -8,7 +7,6 @@ export default {
UploadButton,
DesignVersionDropdown,
},
mixins: [allVersionsMixin],
props: {
isSaving: {
type: Boolean,
......@@ -30,7 +28,7 @@ export default {
<template>
<header class="row-content-block border-top-0 p-2 d-flex">
<div class="d-flex justify-content-between align-items-center w-100">
<design-version-dropdown :all-versions="allVersions" />
<design-version-dropdown />
<upload-button v-if="canUploadDesign" :is-saving="isSaving" @upload="onFileUploadChange" />
</div>
</header>
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import appDataQuery from './graphql/queries/appData.query.graphql';
import projectQuery from './graphql/queries/project.query.graphql';
const genericErrorMessage = s__(
'DesignManagement|An error occurred while loading designs. Please try again.',
);
Vue.use(VueApollo);
const defaultClient = createDefaultClient({
Query: {
design(ctx, { id }, { cache, client }) {
const { projectPath, issueIid } = cache.readQuery({ query: appDataQuery });
return client
.query({
query: projectQuery,
variables: { fullPath: projectPath, iid: issueIid },
})
.then(({ data, errors }) => {
if (errors) {
createFlash(
s__('DesignManagement|An error occurred while loading designs. Please try again.'),
const defaultClient = createDefaultClient(
{
Query: {
design(ctx, { id, version }, { cache, client }) {
const { projectPath, issueIid } = cache.readQuery({ query: appDataQuery });
return client
.query({
query: projectQuery,
variables: {
fullPath: projectPath,
iid: issueIid,
atVersion: version,
},
})
.then(({ data }) => {
const edge = data.project.issue.designs.designs.edges.find(
({ node }) => node.filename === id,
);
throw new Error(errors);
}
const edge = data.project.issue.designs.designs.edges.find(
({ node }) => node.filename === id,
);
return edge.node;
})
.catch(() => {
createFlash(
s__('DesignManagement|An error occurred while loading designs. Please try again.'),
);
});
return edge.node;
})
.catch(() => {
createFlash(genericErrorMessage);
});
},
},
},
});
// This config is added temporarily to resolve an issue with duplicate design IDs.
// Should be removed as soon as https://gitlab.com/gitlab-org/gitlab-ee/issues/13495 is resolved
{
cacheConfig: {
dataIdFromObject: object => {
// eslint-disable-next-line no-underscore-dangle, @gitlab/i18n/no-non-i18n-strings
if (object.__typename === 'Design') {
return object.id && object.image ? `${object.id}-${object.image}` : null;
}
return defaultDataIdFromObject(object);
},
},
},
);
export default new VueApollo({
defaultClient,
......
#import "../fragments/designList.fragment.graphql"
query getDesign($id: String!) {
design(id: $id) @client {
query getDesign($id: String!, $version: String) {
design(id: $id, version: $version) @client {
...DesignListItem
}
}
#import "../fragments/designList.fragment.graphql"
query getVersionDesigns($fullPath: ID!, $iid: String!, $atVersion: ID!) {
project(fullPath: $fullPath) {
id
issue(iid: $iid) {
iid
designs {
designs(atVersion: $atVersion) {
edges {
node {
...DesignListItem
}
}
}
}
}
}
}
#import "../fragments/designList.fragment.graphql"
query project($fullPath: ID!, $iid: String!) {
query project($fullPath: ID!, $iid: String!, $atVersion: ID) {
project(fullPath: $fullPath) {
id
issue(iid: $iid) {
designs {
designs {
designs(atVersion: $atVersion) {
edges {
node {
...DesignListItem
......
......@@ -32,6 +32,7 @@ export default () => {
variables: {
fullPath: projectPath,
iid: issueIid,
atVersion: null,
},
})
.subscribe(({ data }) => {
......
import appDataQuery from '../graphql/queries/appData.query.graphql';
import getVersionDesignsQuery from '../graphql/queries/getVersionDesigns.query.graphql';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import projectQuery from '../graphql/queries/project.query.graphql';
import { extractNodes } from '../utils/design_management_utils';
import allVersionsMixin from './all_versions';
export default {
mixins: [allVersionsMixin],
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
},
designs: {
query: projectQuery,
variables() {
return {
fullPath: this.projectPath,
iid: this.issueIid,
atVersion: this.designsVersion,
};
},
update: data => extractNodes(data.project.issue.designs.designs),
error() {
this.error = true;
},
},
versionDesigns: {
query: getVersionDesignsQuery,
fetchPolicy: 'no-cache',
variables() {
return {
fullPath: this.projectPath,
iid: this.issueIid,
atVersion: `gid://gitlab/DesignManagement::Version/${this.$route.query.version}`,
};
},
skip() {
this.$apollo.queries.versionDesigns.skip = !this.hasValidVersion();
result() {
if (this.$route.query.version && !this.hasValidVersion) {
createFlash(
s__(
'DesignManagement|Requested design version does not exist. Showing latest version instead',
),
);
this.$router.replace({ name: 'designs', query: { version: undefined } });
}
},
update: data => extractNodes(data.project.issue.designs.designs),
},
},
data() {
return {
designs: [],
error: false,
projectPath: '',
issueIid: null,
versionDesigns: [],
};
},
methods: {
hasValidVersion() {
if (Object.keys(this.$route.query).length === 0) {
return false;
}
return this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version));
},
},
};
import projectQuery from '../graphql/queries/project.query.graphql';
import appDataQuery from '../graphql/queries/appData.query.graphql';
export default {
props: {
apollo: {
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath, issueIid } }) {
this.projectPath = projectPath;
this.issueIid = issueIid;
},
},
allVersions: {
type: Array,
required: true,
query: projectQuery,
variables() {
return {
fullPath: this.projectPath,
iid: this.issueIid,
atVersion: null,
};
},
update: data => data.project.issue.designs.versions.edges,
},
},
computed: {
hasValidVersion() {
return (
this.$route.query.version &&
this.allVersions &&
this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version))
);
},
designsVersion() {
return this.hasValidVersion
? `gid://gitlab/DesignManagement::Version/${this.$route.query.version}`
: null;
},
},
data() {
return {
allVersions: [],
projectPath: '',
issueIid: null,
};
},
};
......@@ -3,6 +3,7 @@ import Mousetrap from 'mousetrap';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { GlLoadingIcon } from '@gitlab/ui';
import allVersionsMixin from '../../mixins/all_versions';
import Toolbar from '../../components/toolbar/index.vue';
import DesignImage from '../../components/image.vue';
import DesignOverlay from '../../components/design_overlay.vue';
......@@ -10,7 +11,6 @@ import DesignDiscussion from '../../components/design_notes/design_discussion.vu
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
import appDataQuery from '../../graphql/queries/appData.query.graphql';
import { extractDiscussions } from '../../utils/design_management_utils';
export default {
......@@ -22,6 +22,7 @@ export default {
DesignReplyForm,
GlLoadingIcon,
},
mixins: [allVersionsMixin],
props: {
id: {
type: String,
......@@ -47,20 +48,18 @@ export default {
variables() {
return {
id: this.id,
version: this.designsVersion,
};
},
result({ data }) {
if (!data) {
createFlash(s__('DesignManagement|Could not find design, please try again.'));
this.$router.push('/designs');
this.$router.push({ name: 'designs' });
}
if (this.$route.query.version && !this.hasValidVersion) {
createFlash(s__('DesignManagement|Requested design version does not exist'));
this.$router.push({ name: 'designs' });
}
},
},
appData: {
query: appDataQuery,
manual: true,
result({ data: { projectPath } }) {
this.projectPath = projectPath;
},
},
},
......@@ -120,6 +119,7 @@ export default {
query: getDesignQuery,
variables: {
id: this.id,
version: this.designsVersion,
},
});
const newDiscussion = {
......@@ -175,9 +175,10 @@ export default {
this.overlayDimensions.height = position.height;
},
closeDesign() {
// This needs to be changed to take a design version into account as soon as
// https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/15119 is merged
this.$router.push('/designs');
this.$router.push({
name: 'designs',
query: this.$route.query,
});
},
},
beforeRouteUpdate(to, from, next) {
......
......@@ -33,16 +33,6 @@ export default {
},
update: data => data.project.issue.userPermissions,
},
allVersions: {
query: projectQuery,
variables() {
return {
fullPath: this.projectPath,
iid: this.issueIid,
};
},
update: data => data.project.issue.designs.versions.edges,
},
},
data() {
return {
......@@ -50,7 +40,6 @@ export default {
createDesign: false,
},
isSaving: false,
allVersions: [],
};
},
computed: {
......@@ -66,9 +55,6 @@ export default {
hasDesigns() {
return this.designs.length > 0;
},
hasVersion() {
return this.hasValidVersion();
},
},
methods: {
onUploadDesign(files) {
......@@ -132,7 +118,7 @@ export default {
update: (store, { data: { designManagementUpload } }) => {
const data = store.readQuery({
query: projectQuery,
variables: { fullPath: this.projectPath, iid: this.issueIid },
variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null },
});
const newDesigns = data.project.issue.designs.designs.edges.reduce((acc, design) => {
......@@ -189,7 +175,7 @@ export default {
store.writeQuery({
query: projectQuery,
variables: { fullPath: this.projectPath, iid: this.issueIid },
variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null },
data: newQueryData,
});
},
......@@ -204,7 +190,7 @@ export default {
},
})
.then(() => {
this.$router.push('/designs');
this.$router.push({ name: 'designs' });
})
.catch(e => {
createFlash(s__('DesignManagement|Error uploading a new design. Please try again'));
......@@ -232,7 +218,6 @@ export default {
<div v-else-if="error" class="alert alert-danger">
{{ __('An error occurred while loading designs. Please try again.') }}
</div>
<design-list v-else-if="hasVersion" :designs="versionDesigns" />
<design-list v-else-if="hasDesigns" :designs="designs" />
<gl-empty-state
v-else
......
---
title: Resolve Design viewer does not respect version
merge_request: 15119
author:
type: fixed
......@@ -2,16 +2,17 @@ import { shallowMount } from '@vue/test-utils';
import DesignImage from 'ee/design_management/components/image.vue';
describe('Design management large image component', () => {
let vm;
let wrapper;
function createComponent(propsData) {
vm = shallowMount(DesignImage, {
wrapper = shallowMount(DesignImage, {
sync: false,
propsData,
});
}
afterEach(() => {
vm.destroy();
wrapper.destroy();
});
it('renders loading state', () => {
......@@ -19,7 +20,7 @@ describe('Design management large image component', () => {
isLoading: true,
});
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('renders image', () => {
......@@ -29,6 +30,6 @@ describe('Design management large image component', () => {
name: 'test',
});
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
});
......@@ -10,10 +10,11 @@ const createMockDesign = id => ({
});
describe('Design management list component', () => {
let vm;
let wrapper;
function createComponent() {
vm = shallowMount(List, {
wrapper = shallowMount(List, {
sync: false,
propsData: {
designs: [createMockDesign(1), createMockDesign(2)],
},
......@@ -21,12 +22,12 @@ describe('Design management list component', () => {
}
afterEach(() => {
vm.destroy();
wrapper.destroy();
});
it('renders list', () => {
createComponent();
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount } from '@vue/test-utils';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueRouter from 'vue-router';
import Item from 'ee/design_management/components/list/item.vue';
const localVue = createLocalVue();
localVue.use(VueRouter);
const router = new VueRouter();
describe('Design management list item component', () => {
let vm;
let wrapper;
function createComponent(commentsCount = 1) {
vm = shallowMount(Item, {
wrapper = shallowMount(Item, {
sync: false,
localVue,
router,
propsData: {
id: 1,
name: 'test',
......@@ -18,24 +26,24 @@ describe('Design management list item component', () => {
}
afterEach(() => {
vm.destroy();
wrapper.destroy();
});
it('renders item with single comment', () => {
createComponent();
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('renders item with multiple comments', () => {
createComponent(2);
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('hides comment count', () => {
createComponent(0);
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount } from '@vue/test-utils';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueRouter from 'vue-router';
import Toolbar from 'ee/design_management/components/toolbar/index.vue';
const localVue = createLocalVue();
localVue.use(VueRouter);
const router = new VueRouter();
const RouterLinkStub = {
props: {
to: {
......@@ -13,13 +18,16 @@ const RouterLinkStub = {
};
describe('Design management toolbar component', () => {
let vm;
let wrapper;
function createComponent(isLoading = false) {
const updatedAt = new Date();
updatedAt.setHours(updatedAt.getHours() - 1);
vm = shallowMount(Toolbar, {
wrapper = shallowMount(Toolbar, {
sync: false,
localVue,
router,
propsData: {
id: '1',
isLoading,
......@@ -38,16 +46,19 @@ describe('Design management toolbar component', () => {
it('renders design and updated data', () => {
createComponent();
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('links back to designs list', () => {
createComponent();
const link = vm.find('a');
const link = wrapper.find('a');
expect(link.props('to')).toEqual({
name: 'designs',
query: {
version: undefined,
},
});
});
});
import { shallowMount } from '@vue/test-utils';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueRouter from 'vue-router';
import PaginationButton from 'ee/design_management/components/toolbar/pagination_button.vue';
const localVue = createLocalVue();
localVue.use(VueRouter);
const router = new VueRouter();
describe('Design management pagination button component', () => {
let vm;
let wrapper;
function createComponent(design = null) {
vm = shallowMount(PaginationButton, {
wrapper = shallowMount(PaginationButton, {
sync: false,
localVue,
router,
propsData: {
design,
title: 'Test title',
......@@ -16,34 +24,37 @@ describe('Design management pagination button component', () => {
}
afterEach(() => {
vm.destroy();
wrapper.destroy();
});
it('disables button when no design is passed', () => {
createComponent();
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('renders router-link', () => {
createComponent({ id: '2' });
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
describe('designLink', () => {
it('returns empty link when design is null', () => {
createComponent();
expect(vm.vm.designLink).toEqual({});
expect(wrapper.vm.designLink).toEqual({});
});
it('returns design link', () => {
createComponent({ id: '2', filename: 'test' });
expect(vm.vm.designLink).toEqual({
wrapper.vm.$router.replace('/root/test-project/issues/1/designs/test?version=1');
expect(wrapper.vm.designLink).toEqual({
name: 'design',
params: { id: 'test' },
query: { version: '1' },
});
});
});
......
......@@ -2,10 +2,10 @@ import { shallowMount } from '@vue/test-utils';
import Pagination from 'ee/design_management/components/toolbar/pagination.vue';
describe('Design management pagination component', () => {
let vm;
let wrapper;
function createComponent() {
vm = shallowMount(Pagination, {
wrapper = shallowMount(Pagination, {
propsData: {
id: '2',
},
......@@ -17,18 +17,18 @@ describe('Design management pagination component', () => {
});
afterEach(() => {
vm.destroy();
wrapper.destroy();
});
it('hides components when designs are empty', () => {
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('renders pagination buttons', () => {
vm.setData({
wrapper.setData({
designs: [{ id: '1' }, { id: '2' }],
});
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
});
......@@ -9,9 +9,7 @@ exports[`Design management upload form component hides button if cant upload 1`]
<div
class="d-flex justify-content-between align-items-center w-100"
>
<designversiondropdown-stub
all-versions=""
/>
<designversiondropdown-stub />
<!---->
</div>
......@@ -27,9 +25,7 @@ exports[`Design management upload form component renders upload design button 1`
<div
class="d-flex justify-content-between align-items-center w-100"
>
<designversiondropdown-stub
all-versions=""
/>
<designversiondropdown-stub />
<uploadbutton-stub />
</div>
......
......@@ -2,10 +2,11 @@ import { shallowMount } from '@vue/test-utils';
import UploadButton from 'ee/design_management/components/upload/button.vue';
describe('Design management upload button component', () => {
let vm;
let wrapper;
function createComponent(isSaving = false, isInverted = false) {
vm = shallowMount(UploadButton, {
wrapper = shallowMount(UploadButton, {
sync: false,
propsData: {
isSaving,
isInverted,
......@@ -14,36 +15,38 @@ describe('Design management upload button component', () => {
}
afterEach(() => {
vm.destroy();
wrapper.destroy();
});
it('renders upload design button', () => {
createComponent();
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('renders inverted upload design button', () => {
createComponent(false, true);
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('renders loading icon', () => {
createComponent(true);
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
describe('onFileUploadChange', () => {
it('emits upload event', () => {
createComponent();
jest.spyOn(vm.find({ ref: 'fileUpload' }).element, 'files', 'get').mockReturnValue('test');
jest
.spyOn(wrapper.find({ ref: 'fileUpload' }).element, 'files', 'get')
.mockReturnValue('test');
vm.vm.onFileUploadChange('test');
wrapper.vm.onFileUploadChange('test');
expect(vm.emitted().upload[0]).toEqual(['test']);
expect(wrapper.emitted().upload[0]).toEqual(['test']);
});
});
......@@ -51,9 +54,9 @@ describe('Design management upload button component', () => {
it('triggers click on input', () => {
createComponent();
const clickSpy = jest.spyOn(vm.find({ ref: 'fileUpload' }).element, 'click');
const clickSpy = jest.spyOn(wrapper.find({ ref: 'fileUpload' }).element, 'click');
vm.vm.openFileUpload();
wrapper.vm.openFileUpload();
expect(clickSpy).toHaveBeenCalled();
});
......
......@@ -17,11 +17,14 @@ describe('Design management design version dropdown component', () => {
propsData: {
projectPath: '',
issueIid: '',
allVersions: mockAllVersions,
},
localVue,
router,
});
wrapper.setData({
allVersions: mockAllVersions,
});
}
afterEach(() => {
......
......@@ -2,43 +2,43 @@ import { shallowMount } from '@vue/test-utils';
import UploadForm from 'ee/design_management/components/upload/form.vue';
describe('Design management upload form component', () => {
let vm;
let wrapper;
function createComponent(isSaving = false, canUploadDesign = true) {
vm = shallowMount(UploadForm, {
wrapper = shallowMount(UploadForm, {
sync: false,
propsData: {
isSaving,
canUploadDesign,
projectPath: '',
issueIid: '',
allVersions: [],
},
});
}
afterEach(() => {
vm.destroy();
wrapper.destroy();
});
it('renders upload design button', () => {
createComponent();
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
it('hides button if cant upload', () => {
createComponent(false, false);
expect(vm.element).toMatchSnapshot();
expect(wrapper.element).toMatchSnapshot();
});
describe('onFileUploadChange', () => {
it('emits upload event', () => {
createComponent();
vm.vm.onFileUploadChange('test');
wrapper.vm.onFileUploadChange('test');
expect(vm.emitted().upload[0]).toEqual(['test']);
expect(wrapper.emitted().upload[0]).toEqual(['test']);
});
});
});
......@@ -6,7 +6,15 @@ import uploadDesignQuery from 'ee/design_management/graphql/mutations/uploadDesi
const localVue = createLocalVue();
localVue.use(VueRouter);
const router = new VueRouter();
const router = new VueRouter({
routes: [
{
name: 'designs',
path: '/designs',
component: Index,
},
],
});
describe('Design management index page', () => {
let mutate;
......
......@@ -4990,6 +4990,12 @@ msgstr ""
msgid "DesignManagement|Go to previous design"
msgstr ""
msgid "DesignManagement|Requested design version does not exist"
msgstr ""
msgid "DesignManagement|Requested design version does not exist. Showing latest version instead"
msgstr ""
msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again."
msgstr ""
......
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