Commit c805e676 authored by Luke Duncalfe's avatar Luke Duncalfe Committed by Kushal Pandya

Reflect design collection copy state on frontend

This adds support for seeing the design collection copy state. When an
issue is moved, the designs are moved asynchronously. The user will
initially see a message that their designs are still being copied. If
there is an error during the copy, they will see an error message
displayed but will still be able to upload their designs.

https://gitlab.com/gitlab-org/gitlab/-/issues/13426
parent bea93ce4
...@@ -6,6 +6,7 @@ query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) { ...@@ -6,6 +6,7 @@ query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
id id
issue(iid: $iid) { issue(iid: $iid) {
designCollection { designCollection {
copyState
designs(atVersion: $atVersion) { designs(atVersion: $atVersion) {
nodes { nodes {
...DesignListItem ...DesignListItem
......
...@@ -8,7 +8,7 @@ import { DESIGNS_ROUTE_NAME } from '../router/constants'; ...@@ -8,7 +8,7 @@ import { DESIGNS_ROUTE_NAME } from '../router/constants';
export default { export default {
mixins: [allVersionsMixin], mixins: [allVersionsMixin],
apollo: { apollo: {
designs: { designCollection: {
query: getDesignListQuery, query: getDesignListQuery,
variables() { variables() {
return { return {
...@@ -25,10 +25,11 @@ export default { ...@@ -25,10 +25,11 @@ export default {
'designs', 'designs',
'nodes', 'nodes',
]); ]);
if (designNodes) { const copyState = propertyOf(data)(['project', 'issue', 'designCollection', 'copyState']);
return designNodes; return {
} designs: designNodes,
return []; copyState,
};
}, },
error() { error() {
this.error = true; this.error = true;
...@@ -42,13 +43,26 @@ export default { ...@@ -42,13 +43,26 @@ export default {
); );
this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } }); this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } });
} }
if (this.designCollection.copyState === 'ERROR') {
createFlash(
s__(
'DesignManagement|There was an error moving your designs. Please upload your designs below.',
),
'warning',
);
}
}, },
}, },
}, },
data() { data() {
return { return {
designs: [], designCollection: null,
error: false, error: false,
}; };
}, },
computed: {
designs() {
return this.designCollection?.designs || [];
},
},
}; };
...@@ -75,7 +75,9 @@ export default { ...@@ -75,7 +75,9 @@ export default {
}, },
computed: { computed: {
isLoading() { isLoading() {
return this.$apollo.queries.designs.loading || this.$apollo.queries.permissions.loading; return (
this.$apollo.queries.designCollection.loading || this.$apollo.queries.permissions.loading
);
}, },
isSaving() { isSaving() {
return this.filesToBeSaved.length > 0; return this.filesToBeSaved.length > 0;
...@@ -109,6 +111,11 @@ export default { ...@@ -109,6 +111,11 @@ export default {
isDesignListEmpty() { isDesignListEmpty() {
return !this.isSaving && !this.hasDesigns; return !this.isSaving && !this.hasDesigns;
}, },
isDesignCollectionCopying() {
return (
this.designCollection && ['PENDING', 'COPYING'].includes(this.designCollection.copyState)
);
},
designDropzoneWrapperClass() { designDropzoneWrapperClass() {
return this.isDesignListEmpty return this.isDesignListEmpty
? 'col-12' ? 'col-12'
...@@ -355,6 +362,21 @@ export default { ...@@ -355,6 +362,21 @@ export default {
<gl-alert v-else-if="error" variant="danger" :dismissible="false"> <gl-alert v-else-if="error" variant="danger" :dismissible="false">
{{ __('An error occurred while loading designs. Please try again.') }} {{ __('An error occurred while loading designs. Please try again.') }}
</gl-alert> </gl-alert>
<header
v-else-if="isDesignCollectionCopying"
class="card gl-p-3"
data-testid="design-collection-is-copying"
>
<div class="card-header design-card-header border-bottom-0">
<div class="card-title gl-my-0 gl-h-7">
{{
s__(
'DesignManagement|Your designs are being copied and are on their way… Please refresh to update.',
)
}}
</div>
</div>
</header>
<vue-draggable <vue-draggable
v-else v-else
:value="designs" :value="designs"
......
...@@ -155,6 +155,7 @@ const addNewDesignToStore = (store, designManagementUpload, query) => { ...@@ -155,6 +155,7 @@ const addNewDesignToStore = (store, designManagementUpload, query) => {
const updatedDesigns = { const updatedDesigns = {
__typename: 'DesignCollection', __typename: 'DesignCollection',
copyState: 'READY',
designs: { designs: {
__typename: 'DesignConnection', __typename: 'DesignConnection',
nodes: newDesigns, nodes: newDesigns,
......
...@@ -65,6 +65,10 @@ export const designUploadOptimisticResponse = files => { ...@@ -65,6 +65,10 @@ export const designUploadOptimisticResponse = files => {
fullPath: '', fullPath: '',
notesCount: 0, notesCount: 0,
event: 'NONE', event: 'NONE',
currentUserTodos: {
__typename: 'TodoConnection',
nodes: [],
},
diffRefs: { diffRefs: {
__typename: 'DiffRefs', __typename: 'DiffRefs',
baseSha: '', baseSha: '',
......
...@@ -152,6 +152,10 @@ ...@@ -152,6 +152,10 @@
} }
} }
.design-card-header {
background: transparent;
}
.design-dropzone-border { .design-dropzone-border {
border: 2px dashed $gray-100; border: 2px dashed $gray-100;
} }
......
---
title: Copy designs to issue when an issue with designs is moved
merge_request: 42548
author:
type: fixed
...@@ -8,6 +8,8 @@ msgid "" ...@@ -8,6 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-18 11:00+1200\n"
"PO-Revision-Date: 2020-09-18 11:00+1200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -8845,6 +8847,9 @@ msgstr "" ...@@ -8845,6 +8847,9 @@ msgstr ""
msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again." msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again."
msgstr "" msgstr ""
msgid "DesignManagement|There was an error moving your designs. Please upload your designs below."
msgstr ""
msgid "DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}" msgid "DesignManagement|To upload designs, you'll need to enable LFS and have admin enable hashed storage. %{requirements_link_start}More information%{requirements_link_end}"
msgstr "" msgstr ""
...@@ -8857,6 +8862,9 @@ msgstr "" ...@@ -8857,6 +8862,9 @@ msgstr ""
msgid "DesignManagement|Upload skipped." msgid "DesignManagement|Upload skipped."
msgstr "" msgstr ""
msgid "DesignManagement|Your designs are being copied and are on their way… Please refresh to update."
msgstr ""
msgid "DesignManagement|and %{moreCount} more." msgid "DesignManagement|and %{moreCount} more."
msgstr "" msgstr ""
......
...@@ -43,7 +43,7 @@ describe('Design management pagination component', () => { ...@@ -43,7 +43,7 @@ describe('Design management pagination component', () => {
it('renders navigation buttons', () => { it('renders navigation buttons', () => {
wrapper.setData({ wrapper.setData({
designs: [{ id: '1' }, { id: '2' }], designCollection: { designs: [{ id: '1' }, { id: '2' }] },
}); });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
...@@ -54,7 +54,7 @@ describe('Design management pagination component', () => { ...@@ -54,7 +54,7 @@ describe('Design management pagination component', () => {
describe('keyboard buttons navigation', () => { describe('keyboard buttons navigation', () => {
beforeEach(() => { beforeEach(() => {
wrapper.setData({ wrapper.setData({
designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }], designCollection: { designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }] },
}); });
}); });
......
...@@ -4,6 +4,7 @@ export const designListQueryResponse = { ...@@ -4,6 +4,7 @@ export const designListQueryResponse = {
id: '1', id: '1',
issue: { issue: {
designCollection: { designCollection: {
copyState: 'READY',
designs: { designs: {
nodes: [ nodes: [
{ {
......
...@@ -92,6 +92,8 @@ describe('Design management index page', () => { ...@@ -92,6 +92,8 @@ 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 findToolbar = () => wrapper.find('.qa-selector-toolbar'); const findToolbar = () => wrapper.find('.qa-selector-toolbar');
const findDesignCollectionIsCopying = () =>
wrapper.find('[data-testid="design-collection-is-copying"');
const findDeleteButton = () => wrapper.find(DeleteButton); const findDeleteButton = () => wrapper.find(DeleteButton);
const findDropzone = () => wrapper.findAll(DesignDropzone).at(0); const findDropzone = () => wrapper.findAll(DesignDropzone).at(0);
const dropzoneClasses = () => findDropzone().classes(); const dropzoneClasses = () => findDropzone().classes();
...@@ -115,8 +117,8 @@ describe('Design management index page', () => { ...@@ -115,8 +117,8 @@ describe('Design management index page', () => {
function createComponent({ function createComponent({
loading = false, loading = false,
designs = [],
allVersions = [], allVersions = [],
designCollection = { designs: mockDesigns, copyState: 'READY' },
createDesign = true, createDesign = true,
stubs = {}, stubs = {},
mockMutate = jest.fn().mockResolvedValue(), mockMutate = jest.fn().mockResolvedValue(),
...@@ -124,7 +126,7 @@ describe('Design management index page', () => { ...@@ -124,7 +126,7 @@ describe('Design management index page', () => {
mutate = mockMutate; mutate = mockMutate;
const $apollo = { const $apollo = {
queries: { queries: {
designs: { designCollection: {
loading, loading,
}, },
permissions: { permissions: {
...@@ -137,8 +139,8 @@ describe('Design management index page', () => { ...@@ -137,8 +139,8 @@ describe('Design management index page', () => {
wrapper = shallowMount(Index, { wrapper = shallowMount(Index, {
data() { data() {
return { return {
designs,
allVersions, allVersions,
designCollection,
permissions: { permissions: {
createDesign, createDesign,
}, },
...@@ -200,13 +202,13 @@ describe('Design management index page', () => { ...@@ -200,13 +202,13 @@ describe('Design management index page', () => {
}); });
it('renders a toolbar with buttons when there are designs', () => { it('renders a toolbar with buttons when there are designs', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); createComponent({ allVersions: [mockVersion] });
expect(findToolbar().exists()).toBe(true); expect(findToolbar().exists()).toBe(true);
}); });
it('renders designs list and header with upload button', () => { it('renders designs list and header with upload button', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); createComponent({ allVersions: [mockVersion] });
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
...@@ -236,7 +238,7 @@ describe('Design management index page', () => { ...@@ -236,7 +238,7 @@ describe('Design management index page', () => {
describe('when has no designs', () => { describe('when has no designs', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({ designCollection: { designs: [], copyState: 'READY' } });
}); });
it('renders design dropzone', () => it('renders design dropzone', () =>
...@@ -259,6 +261,21 @@ describe('Design management index page', () => { ...@@ -259,6 +261,21 @@ describe('Design management index page', () => {
})); }));
}); });
describe('handling design collection copy state', () => {
it.each`
copyState | isRendered | description
${'COPYING'} | ${true} | ${'renders'}
${'READY'} | ${false} | ${'does not render'}
${'ERROR'} | ${false} | ${'does not render'}
`(
'$description the copying message if design collection copyState is $copyState',
({ copyState, isRendered }) => {
createComponent({ designCollection: { designs: [], copyState } });
expect(findDesignCollectionIsCopying().exists()).toBe(isRendered);
},
);
});
describe('uploading designs', () => { describe('uploading designs', () => {
it('calls mutation on upload', () => { it('calls mutation on upload', () => {
createComponent({ stubs: { GlEmptyState } }); createComponent({ stubs: { GlEmptyState } });
...@@ -282,6 +299,10 @@ describe('Design management index page', () => { ...@@ -282,6 +299,10 @@ describe('Design management index page', () => {
{ {
__typename: 'Design', __typename: 'Design',
id: expect.anything(), id: expect.anything(),
currentUserTodos: {
__typename: 'TodoConnection',
nodes: [],
},
image: '', image: '',
imageV432x230: '', imageV432x230: '',
filename: 'test', filename: 'test',
...@@ -531,13 +552,16 @@ describe('Design management index page', () => { ...@@ -531,13 +552,16 @@ describe('Design management index page', () => {
}); });
it('on latest version when has no designs toolbar buttons are invisible', () => { it('on latest version when has no designs toolbar buttons are invisible', () => {
createComponent({ designs: [], allVersions: [mockVersion] }); createComponent({
designCollection: { designs: [], copyState: 'READY' },
allVersions: [mockVersion],
});
expect(findToolbar().isVisible()).toBe(false); expect(findToolbar().isVisible()).toBe(false);
}); });
describe('on non-latest version', () => { describe('on non-latest version', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] }); createComponent({ allVersions: [mockVersion] });
}); });
it('does not render design checkboxes', async () => { it('does not render design checkboxes', async () => {
......
...@@ -25,7 +25,7 @@ function factory(routeArg) { ...@@ -25,7 +25,7 @@ function factory(routeArg) {
mocks: { mocks: {
$apollo: { $apollo: {
queries: { queries: {
designs: { loading: true }, designCollection: { loading: true },
design: { loading: true }, design: { loading: true },
permissions: { loading: true }, permissions: { loading: true },
}, },
......
...@@ -93,6 +93,10 @@ describe('optimistic responses', () => { ...@@ -93,6 +93,10 @@ describe('optimistic responses', () => {
fullPath: '', fullPath: '',
notesCount: 0, notesCount: 0,
event: 'NONE', event: 'NONE',
currentUserTodos: {
__typename: 'TodoConnection',
nodes: [],
},
diffRefs: { __typename: 'DiffRefs', baseSha: '', startSha: '', headSha: '' }, diffRefs: { __typename: 'DiffRefs', baseSha: '', startSha: '', headSha: '' },
discussions: { __typename: 'DesignDiscussion', nodes: [] }, discussions: { __typename: 'DesignDiscussion', nodes: [] },
versions: { versions: {
......
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