Commit 93a77b70 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'design-management-upload-component' into 'master'

Created upload designs component

See merge request gitlab-org/gitlab-ee!10017
parents dfa9e89e 89be6c60
...@@ -18,6 +18,7 @@ export default { ...@@ -18,6 +18,7 @@ export default {
<ol class="list-unstyled row"> <ol class="list-unstyled row">
<li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4"> <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4">
<design <design
:id="design.id"
:comments-count="design.commentsCount" :comments-count="design.commentsCount"
:image="design.image" :image="design.image"
:name="design.name" :name="design.name"
......
...@@ -9,6 +9,10 @@ export default { ...@@ -9,6 +9,10 @@ export default {
Timeago, Timeago,
}, },
props: { props: {
id: {
type: Number,
required: true,
},
commentsCount: { commentsCount: {
type: Number, type: Number,
required: true, required: true,
...@@ -35,21 +39,32 @@ export default { ...@@ -35,21 +39,32 @@ export default {
</script> </script>
<template> <template>
<div class="card js-design-list-item"> <router-link
:to="{ name: 'design', params: { id } }"
class="card cursor-pointer text-plain js-design-list-item"
>
<div class="card-body p-0"> <div class="card-body p-0">
<img :src="image" :alt="name" class="block ml-auto mr-auto design-img" height="230" /> <img :src="image" :alt="name" class="block ml-auto mr-auto mw-100 design-img" height="230" />
</div> </div>
<div class="card-footer d-flex"> <div class="card-footer d-flex w-100">
<div class="d-flex flex-column"> <div class="d-flex flex-column str-truncated-100">
<span class="bold">{{ name }}</span> <span class="bold str-truncated-100">{{ name }}</span>
<span>{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom"/></span> <span class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
</div> </div>
<div class="ml-auto d-flex align-items-center text-secondary"> <div v-if="commentsCount" class="ml-auto d-flex align-items-center text-secondary">
<icon name="comments" /> <icon name="comments" class="ml-1" />
<span :aria-label="commentsLabel" class="ml-1"> <span :aria-label="commentsLabel" class="ml-1">
{{ commentsCount }} {{ commentsCount }}
</span> </span>
</div> </div>
</div> </div>
</div> </router-link>
</template> </template>
<style scoped>
.card:hover {
text-decoration: none;
}
</style>
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlButton,
GlLoadingIcon,
},
props: {
isSaving: {
type: Boolean,
required: true,
},
},
methods: {
openFileUpload() {
this.$refs.fileUpload.click();
},
onFileUploadChange() {
this.$emit('upload', this.$refs.fileUpload.files);
},
},
};
</script>
<template>
<header class="row-content-block border-top-0 pt-2 pb-2 pl-0 pr-0 d-flex">
<div class="ml-auto">
<gl-button :disabled="isSaving" variant="primary" @click="openFileUpload">
{{ s__('DesignManagement|Upload new designs') }}
<gl-loading-icon v-if="isSaving" inline class="ml-1" />
</gl-button>
<input
ref="fileUpload"
type="file"
name="design_file"
accept="image/*"
class="hide"
@change="onFileUploadChange"
/>
</div>
</header>
</template>
...@@ -6,7 +6,7 @@ import createDefaultClient from '~/lib/graphql'; ...@@ -6,7 +6,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo); Vue.use(VueApollo);
const createMockDesign = id => ({ const createMockDesign = id => ({
id, id: Number(id),
image: 'http://via.placeholder.com/300', image: 'http://via.placeholder.com/300',
name: 'test.jpg', name: 'test.jpg',
commentsCount: 2, commentsCount: 2,
...@@ -25,5 +25,20 @@ export default new VueApollo({ ...@@ -25,5 +25,20 @@ export default new VueApollo({
createMockDesign(_.uniqueId()), createMockDesign(_.uniqueId()),
], ],
}, },
resolvers: {
Mutation: {
uploadDesign(ctx, { name }, { cache }) {
const design = {
...createMockDesign(_.uniqueId()),
name,
commentsCount: 0,
};
cache.writeData({ data: design });
return design;
},
},
},
}), }),
}); });
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import DesignList from '../components/list/index.vue'; import DesignList from '../components/list/index.vue';
import UploadForm from '../components/upload/form.vue';
import allDesignsQuery from '../queries/allDesigns.graphql'; import allDesignsQuery from '../queries/allDesigns.graphql';
import uploadDesignQuery from '../queries/uploadDesign.graphql';
export default { export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
DesignList, DesignList,
UploadForm,
}, },
apollo: { apollo: {
designs: { designs: {
...@@ -20,6 +25,7 @@ export default { ...@@ -20,6 +25,7 @@ export default {
return { return {
designs: [], designs: [],
error: false, error: false,
isSaving: false,
}; };
}, },
computed: { computed: {
...@@ -27,13 +33,56 @@ export default { ...@@ -27,13 +33,56 @@ export default {
return this.$apollo.queries.designs.loading; return this.$apollo.queries.designs.loading;
}, },
}, },
methods: {
onUploadDesign(files) {
const file = files[0];
this.isSaving = true;
return this.$apollo
.mutate({
mutation: uploadDesignQuery,
variables: {
name: file.name,
},
update: (store, { data: { uploadDesign } }) => {
const data = store.readQuery({ query: allDesignsQuery });
data.designs.unshift(uploadDesign);
store.writeQuery({ query: allDesignsQuery, data });
},
optimisticResponse: {
__typename: 'Mutation',
uploadDesign: {
__typename: 'Design',
id: -1,
image: '',
name: file.name,
commentsCount: 0,
updatedAt: new Date().toString(),
},
},
})
.then(() => {
this.isSaving = false;
})
.catch(e => {
this.isSaving = false;
createFlash(s__('DesignManagement|Error uploading a new design. Please try again'));
throw e;
});
},
},
}; };
</script> </script>
<template> <template>
<div> <div>
<upload-form :is-saving="isSaving" @upload="onUploadDesign" />
<div class="mt-4"> <div class="mt-4">
<gl-loading-icon v-if="isLoading" :size="2" /> <gl-loading-icon v-if="isLoading" size="md" />
<div v-else-if="error" class="alert alert-danger"> <div v-else-if="error" class="alert alert-danger">
{{ __('An error occurred while loading designs. Please try again.') }} {{ __('An error occurred while loading designs. Please try again.') }}
</div> </div>
......
mutation addDesigns($name: String) {
uploadDesign(name: $name) @client {
id
image
name
updatedAt
commentsCount
}
}
require 'spec_helper'
describe 'User uploads new design', :js do
let(:project) { create(:project_empty_repo, :public) }
let(:issue) { create(:issue, project: project) }
before do
visit project_issue_path(project, issue)
click_link 'Designs'
wait_for_requests
end
it 'uploads design' do
attach_file(:design_file, logo_fixture, make_visible: true)
expect(page).to have_selector('.js-design-list-item', count: 6)
within first('#designs-tab .card') do
expect(page).to have_content('dk.png')
expect(page).to have_content('Updated just now')
end
end
def logo_fixture
Rails.root.join('spec', 'fixtures', 'dk.png')
end
end
...@@ -9,6 +9,7 @@ exports[`Design management list component renders list 1`] = ` ...@@ -9,6 +9,7 @@ exports[`Design management list component renders list 1`] = `
> >
<design-stub <design-stub
commentscount="2" commentscount="2"
id="1"
image="test" image="test"
name="test" name="test"
updatedat="01-01-2019" updatedat="01-01-2019"
...@@ -19,6 +20,7 @@ exports[`Design management list component renders list 1`] = ` ...@@ -19,6 +20,7 @@ exports[`Design management list component renders list 1`] = `
> >
<design-stub <design-stub
commentscount="2" commentscount="2"
id="2"
image="test" image="test"
name="test" name="test"
updatedat="01-01-2019" updatedat="01-01-2019"
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management list item component hides comment count 1`] = `
<router-link-stub
class="card cursor-pointer text-plain js-design-list-item"
to="[object Object]"
>
<div
class="card-body p-0"
>
<img
alt="test"
class="block ml-auto mr-auto mw-100 design-img"
height="230"
src="http://via.placeholder.com/300"
/>
</div>
<div
class="card-footer d-flex w-100"
>
<div
class="d-flex flex-column str-truncated-100"
>
<span
class="bold str-truncated-100"
>
test
</span>
<span
class="str-truncated-100"
>
Updated
<timeago-stub
cssclass=""
time="01-01-2019"
tooltipplacement="bottom"
/>
</span>
</div>
<!---->
</div>
</router-link-stub>
`;
exports[`Design management list item component renders item with multiple comments 1`] = ` exports[`Design management list item component renders item with multiple comments 1`] = `
<div <router-link-stub
class="card js-design-list-item" class="card cursor-pointer text-plain js-design-list-item"
id="1" to="[object Object]"
> >
<div <div
class="card-body p-0" class="card-body p-0"
> >
<img <img
alt="test" alt="test"
class="block ml-auto mr-auto design-img" class="block ml-auto mr-auto mw-100 design-img"
height="230" height="230"
src="http://via.placeholder.com/300" src="http://via.placeholder.com/300"
/> />
</div> </div>
<div <div
class="card-footer d-flex" class="card-footer d-flex w-100"
> >
<div <div
class="d-flex flex-column" class="d-flex flex-column str-truncated-100"
> >
<span <span
class="bold" class="bold str-truncated-100"
> >
test test
</span> </span>
<span> <span
class="str-truncated-100"
>
Updated Updated
<timeago-stub <timeago-stub
cssclass="" cssclass=""
...@@ -42,6 +91,7 @@ exports[`Design management list item component renders item with multiple commen ...@@ -42,6 +91,7 @@ exports[`Design management list item component renders item with multiple commen
class="ml-auto d-flex align-items-center text-secondary" class="ml-auto d-flex align-items-center text-secondary"
> >
<icon-stub <icon-stub
class="ml-1"
cssclasses="" cssclasses=""
name="comments" name="comments"
size="16" size="16"
...@@ -57,38 +107,41 @@ exports[`Design management list item component renders item with multiple commen ...@@ -57,38 +107,41 @@ exports[`Design management list item component renders item with multiple commen
</span> </span>
</div> </div>
</div> </div>
</div> </router-link-stub>
`; `;
exports[`Design management list item component renders item with single comment 1`] = ` exports[`Design management list item component renders item with single comment 1`] = `
<div <router-link-stub
class="card js-design-list-item" class="card cursor-pointer text-plain js-design-list-item"
id="1" to="[object Object]"
> >
<div <div
class="card-body p-0" class="card-body p-0"
> >
<img <img
alt="test" alt="test"
class="block ml-auto mr-auto design-img" class="block ml-auto mr-auto mw-100 design-img"
height="230" height="230"
src="http://via.placeholder.com/300" src="http://via.placeholder.com/300"
/> />
</div> </div>
<div <div
class="card-footer d-flex" class="card-footer d-flex w-100"
> >
<div <div
class="d-flex flex-column" class="d-flex flex-column str-truncated-100"
> >
<span <span
class="bold" class="bold str-truncated-100"
> >
test test
</span> </span>
<span> <span
class="str-truncated-100"
>
Updated Updated
<timeago-stub <timeago-stub
cssclass="" cssclass=""
...@@ -102,6 +155,7 @@ exports[`Design management list item component renders item with single comment ...@@ -102,6 +155,7 @@ exports[`Design management list item component renders item with single comment
class="ml-auto d-flex align-items-center text-secondary" class="ml-auto d-flex align-items-center text-secondary"
> >
<icon-stub <icon-stub
class="ml-1"
cssclasses="" cssclasses=""
name="comments" name="comments"
size="16" size="16"
...@@ -117,5 +171,5 @@ exports[`Design management list item component renders item with single comment ...@@ -117,5 +171,5 @@ exports[`Design management list item component renders item with single comment
</span> </span>
</div> </div>
</div> </div>
</div> </router-link-stub>
`; `;
...@@ -4,7 +4,7 @@ import Item from 'ee/design_management/components/list/item.vue'; ...@@ -4,7 +4,7 @@ import Item from 'ee/design_management/components/list/item.vue';
describe('Design management list item component', () => { describe('Design management list item component', () => {
let vm; let vm;
function createComponent(commentsCount) { function createComponent(commentsCount = 1) {
vm = shallowMount(Item, { vm = shallowMount(Item, {
propsData: { propsData: {
id: 1, id: 1,
...@@ -13,6 +13,7 @@ describe('Design management list item component', () => { ...@@ -13,6 +13,7 @@ describe('Design management list item component', () => {
commentsCount, commentsCount,
updatedAt: '01-01-2019', updatedAt: '01-01-2019',
}, },
stubs: ['router-link'],
}); });
} }
...@@ -21,7 +22,7 @@ describe('Design management list item component', () => { ...@@ -21,7 +22,7 @@ describe('Design management list item component', () => {
}); });
it('renders item with single comment', () => { it('renders item with single comment', () => {
createComponent(1); createComponent();
expect(vm.element).toMatchSnapshot(); expect(vm.element).toMatchSnapshot();
}); });
...@@ -31,4 +32,10 @@ describe('Design management list item component', () => { ...@@ -31,4 +32,10 @@ describe('Design management list item component', () => {
expect(vm.element).toMatchSnapshot(); expect(vm.element).toMatchSnapshot();
}); });
it('hides comment count', () => {
createComponent(0);
expect(vm.element).toMatchSnapshot();
});
}); });
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management upload form component renders loading icon 1`] = `
<header
class="row-content-block border-top-0 pt-2 pb-2 pl-0 pr-0 d-flex"
>
<div
class="ml-auto"
>
<glbutton-stub
disabled="true"
variant="primary"
>
Upload new designs
<glloadingicon-stub
class="ml-1"
color="orange"
inline="true"
label="Loading"
size="sm"
/>
</glbutton-stub>
<input
accept="image/*"
class="hide"
name="design_file"
type="file"
/>
</div>
</header>
`;
exports[`Design management upload form component renders upload design button 1`] = `
<header
class="row-content-block border-top-0 pt-2 pb-2 pl-0 pr-0 d-flex"
>
<div
class="ml-auto"
>
<glbutton-stub
variant="primary"
>
Upload new designs
<!---->
</glbutton-stub>
<input
accept="image/*"
class="hide"
name="design_file"
type="file"
/>
</div>
</header>
`;
import { shallowMount } from '@vue/test-utils';
import UploadForm from 'ee/design_management/components/upload/form.vue';
describe('Design management upload form component', () => {
let vm;
function createComponent(isSaving = false) {
vm = shallowMount(UploadForm, {
propsData: {
isSaving,
},
});
}
it('renders upload design button', () => {
createComponent();
expect(vm.element).toMatchSnapshot();
});
it('renders loading icon', () => {
createComponent(true);
expect(vm.element).toMatchSnapshot();
});
describe('onFileUploadChange', () => {
it('emits upload event', () => {
createComponent();
jest.spyOn(vm.find({ ref: 'fileUpload' }).element, 'files', 'get').mockReturnValue('test');
vm.vm.onFileUploadChange();
expect(vm.emitted().upload[0]).toEqual(['test']);
});
});
describe('openFileUpload', () => {
it('triggers click on input', () => {
createComponent();
const clickSpy = jest.spyOn(vm.find({ ref: 'fileUpload' }).element, 'click');
vm.vm.openFileUpload();
expect(clickSpy).toHaveBeenCalled();
});
});
});
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
exports[`Design management index page designs renders designs list 1`] = ` exports[`Design management index page designs renders designs list 1`] = `
<div> <div>
<uploadform-stub />
<div <div
class="mt-4" class="mt-4"
> >
...@@ -14,6 +16,8 @@ exports[`Design management index page designs renders designs list 1`] = ` ...@@ -14,6 +16,8 @@ exports[`Design management index page designs renders designs list 1`] = `
exports[`Design management index page designs renders empty text 1`] = ` exports[`Design management index page designs renders empty text 1`] = `
<div> <div>
<uploadform-stub />
<div <div
class="mt-4" class="mt-4"
> >
...@@ -26,6 +30,8 @@ exports[`Design management index page designs renders empty text 1`] = ` ...@@ -26,6 +30,8 @@ exports[`Design management index page designs renders empty text 1`] = `
exports[`Design management index page designs renders error 1`] = ` exports[`Design management index page designs renders error 1`] = `
<div> <div>
<uploadform-stub />
<div <div
class="mt-4" class="mt-4"
> >
...@@ -42,13 +48,15 @@ exports[`Design management index page designs renders error 1`] = ` ...@@ -42,13 +48,15 @@ exports[`Design management index page designs renders error 1`] = `
exports[`Design management index page designs renders loading icon 1`] = ` exports[`Design management index page designs renders loading icon 1`] = `
<div> <div>
<uploadform-stub />
<div <div
class="mt-4" class="mt-4"
> >
<glloadingicon-stub <glloadingicon-stub
color="orange" color="orange"
label="Loading" label="Loading"
size="2" size="md"
/> />
</div> </div>
</div> </div>
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Index from 'ee/design_management/pages/index.vue'; import Index from 'ee/design_management/pages/index.vue';
import uploadDesignQuery from 'ee/design_management/queries/uploadDesign.graphql';
describe('Design management index page', () => { describe('Design management index page', () => {
const mutate = jest.fn(() => Promise.resolve());
let vm; let vm;
function createComponent(loading = false) { function createComponent(loading = false) {
...@@ -11,6 +13,7 @@ describe('Design management index page', () => { ...@@ -11,6 +13,7 @@ describe('Design management index page', () => {
loading, loading,
}, },
}, },
mutate,
}; };
vm = shallowMount(Index, { vm = shallowMount(Index, {
...@@ -47,4 +50,53 @@ describe('Design management index page', () => { ...@@ -47,4 +50,53 @@ describe('Design management index page', () => {
expect(vm.element).toMatchSnapshot(); expect(vm.element).toMatchSnapshot();
}); });
}); });
describe('onUploadDesign', () => {
it('calls apollo mutate', () => {
createComponent();
return vm.vm
.onUploadDesign([
{
name: 'test',
},
])
.then(() => {
expect(mutate).toHaveBeenCalledWith({
mutation: uploadDesignQuery,
update: expect.any(Function),
variables: {
name: 'test',
},
optimisticResponse: {
__typename: 'Mutation',
uploadDesign: {
__typename: 'Design',
id: -1,
image: '',
name: 'test',
commentsCount: 0,
updatedAt: expect.any(String),
},
},
});
});
});
it('sets isSaving', () => {
createComponent();
const uploadDesign = vm.vm.onUploadDesign([
{
name: 'test',
},
]);
expect(vm.vm.isSaving).toBe(true);
return uploadDesign.then(() => {
expect(vm.vm.isSaving).toBe(false);
});
});
});
}); });
...@@ -3439,6 +3439,12 @@ msgstr "" ...@@ -3439,6 +3439,12 @@ msgstr ""
msgid "Description:" msgid "Description:"
msgstr "" msgstr ""
msgid "DesignManagement|Error uploading a new design. Please try again"
msgstr ""
msgid "DesignManagement|Upload new designs"
msgstr ""
msgid "Designs" msgid "Designs"
msgstr "" 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