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) {
id
issue(iid: $iid) {
designCollection {
copyState
designs(atVersion: $atVersion) {
nodes {
...DesignListItem
......
......@@ -8,7 +8,7 @@ import { DESIGNS_ROUTE_NAME } from '../router/constants';
export default {
mixins: [allVersionsMixin],
apollo: {
designs: {
designCollection: {
query: getDesignListQuery,
variables() {
return {
......@@ -25,10 +25,11 @@ export default {
'designs',
'nodes',
]);
if (designNodes) {
return designNodes;
}
return [];
const copyState = propertyOf(data)(['project', 'issue', 'designCollection', 'copyState']);
return {
designs: designNodes,
copyState,
};
},
error() {
this.error = true;
......@@ -42,13 +43,26 @@ export default {
);
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() {
return {
designs: [],
designCollection: null,
error: false,
};
},
computed: {
designs() {
return this.designCollection?.designs || [];
},
},
};
......@@ -75,7 +75,9 @@ export default {
},
computed: {
isLoading() {
return this.$apollo.queries.designs.loading || this.$apollo.queries.permissions.loading;
return (
this.$apollo.queries.designCollection.loading || this.$apollo.queries.permissions.loading
);
},
isSaving() {
return this.filesToBeSaved.length > 0;
......@@ -109,6 +111,11 @@ export default {
isDesignListEmpty() {
return !this.isSaving && !this.hasDesigns;
},
isDesignCollectionCopying() {
return (
this.designCollection && ['PENDING', 'COPYING'].includes(this.designCollection.copyState)
);
},
designDropzoneWrapperClass() {
return this.isDesignListEmpty
? 'col-12'
......@@ -355,6 +362,21 @@ export default {
<gl-alert v-else-if="error" variant="danger" :dismissible="false">
{{ __('An error occurred while loading designs. Please try again.') }}
</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
v-else
:value="designs"
......
......@@ -155,6 +155,7 @@ const addNewDesignToStore = (store, designManagementUpload, query) => {
const updatedDesigns = {
__typename: 'DesignCollection',
copyState: 'READY',
designs: {
__typename: 'DesignConnection',
nodes: newDesigns,
......
......@@ -65,6 +65,10 @@ export const designUploadOptimisticResponse = files => {
fullPath: '',
notesCount: 0,
event: 'NONE',
currentUserTodos: {
__typename: 'TodoConnection',
nodes: [],
},
diffRefs: {
__typename: 'DiffRefs',
baseSha: '',
......
......@@ -152,6 +152,10 @@
}
}
.design-card-header {
background: transparent;
}
.design-dropzone-border {
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 ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\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"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
......@@ -8845,6 +8847,9 @@ msgstr ""
msgid "DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again."
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}"
msgstr ""
......@@ -8857,6 +8862,9 @@ msgstr ""
msgid "DesignManagement|Upload skipped."
msgstr ""
msgid "DesignManagement|Your designs are being copied and are on their way… Please refresh to update."
msgstr ""
msgid "DesignManagement|and %{moreCount} more."
msgstr ""
......
......@@ -43,7 +43,7 @@ describe('Design management pagination component', () => {
it('renders navigation buttons', () => {
wrapper.setData({
designs: [{ id: '1' }, { id: '2' }],
designCollection: { designs: [{ id: '1' }, { id: '2' }] },
});
return wrapper.vm.$nextTick().then(() => {
......@@ -54,7 +54,7 @@ describe('Design management pagination component', () => {
describe('keyboard buttons navigation', () => {
beforeEach(() => {
wrapper.setData({
designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }],
designCollection: { designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }] },
});
});
......
......@@ -4,6 +4,7 @@ export const designListQueryResponse = {
id: '1',
issue: {
designCollection: {
copyState: 'READY',
designs: {
nodes: [
{
......
......@@ -92,6 +92,8 @@ describe('Design management index page', () => {
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
const findSelectAllButton = () => wrapper.find('.js-select-all');
const findToolbar = () => wrapper.find('.qa-selector-toolbar');
const findDesignCollectionIsCopying = () =>
wrapper.find('[data-testid="design-collection-is-copying"');
const findDeleteButton = () => wrapper.find(DeleteButton);
const findDropzone = () => wrapper.findAll(DesignDropzone).at(0);
const dropzoneClasses = () => findDropzone().classes();
......@@ -115,8 +117,8 @@ describe('Design management index page', () => {
function createComponent({
loading = false,
designs = [],
allVersions = [],
designCollection = { designs: mockDesigns, copyState: 'READY' },
createDesign = true,
stubs = {},
mockMutate = jest.fn().mockResolvedValue(),
......@@ -124,7 +126,7 @@ describe('Design management index page', () => {
mutate = mockMutate;
const $apollo = {
queries: {
designs: {
designCollection: {
loading,
},
permissions: {
......@@ -137,8 +139,8 @@ describe('Design management index page', () => {
wrapper = shallowMount(Index, {
data() {
return {
designs,
allVersions,
designCollection,
permissions: {
createDesign,
},
......@@ -200,13 +202,13 @@ describe('Design management index page', () => {
});
it('renders a toolbar with buttons when there are designs', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
createComponent({ allVersions: [mockVersion] });
expect(findToolbar().exists()).toBe(true);
});
it('renders designs list and header with upload button', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
createComponent({ allVersions: [mockVersion] });
expect(wrapper.element).toMatchSnapshot();
});
......@@ -236,7 +238,7 @@ describe('Design management index page', () => {
describe('when has no designs', () => {
beforeEach(() => {
createComponent();
createComponent({ designCollection: { designs: [], copyState: 'READY' } });
});
it('renders design dropzone', () =>
......@@ -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', () => {
it('calls mutation on upload', () => {
createComponent({ stubs: { GlEmptyState } });
......@@ -282,6 +299,10 @@ describe('Design management index page', () => {
{
__typename: 'Design',
id: expect.anything(),
currentUserTodos: {
__typename: 'TodoConnection',
nodes: [],
},
image: '',
imageV432x230: '',
filename: 'test',
......@@ -531,13 +552,16 @@ describe('Design management index page', () => {
});
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);
});
describe('on non-latest version', () => {
beforeEach(() => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
createComponent({ allVersions: [mockVersion] });
});
it('does not render design checkboxes', async () => {
......
......@@ -25,7 +25,7 @@ function factory(routeArg) {
mocks: {
$apollo: {
queries: {
designs: { loading: true },
designCollection: { loading: true },
design: { loading: true },
permissions: { loading: true },
},
......
......@@ -93,6 +93,10 @@ describe('optimistic responses', () => {
fullPath: '',
notesCount: 0,
event: 'NONE',
currentUserTodos: {
__typename: 'TodoConnection',
nodes: [],
},
diffRefs: { __typename: 'DiffRefs', baseSha: '', startSha: '', headSha: '' },
discussions: { __typename: 'DesignDiscussion', nodes: [] },
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