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 {
<ol class="list-unstyled row">
<li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4">
<design
:id="design.id"
:comments-count="design.commentsCount"
:image="design.image"
:name="design.name"
......
......@@ -9,6 +9,10 @@ export default {
Timeago,
},
props: {
id: {
type: Number,
required: true,
},
commentsCount: {
type: Number,
required: true,
......@@ -35,21 +39,32 @@ export default {
</script>
<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">
<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 class="card-footer d-flex">
<div class="d-flex flex-column">
<span class="bold">{{ name }}</span>
<span>{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom"/></span>
<div class="card-footer d-flex w-100">
<div class="d-flex flex-column str-truncated-100">
<span class="bold str-truncated-100">{{ name }}</span>
<span class="str-truncated-100">
{{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
</span>
</div>
<div class="ml-auto d-flex align-items-center text-secondary">
<icon name="comments" />
<div v-if="commentsCount" class="ml-auto d-flex align-items-center text-secondary">
<icon name="comments" class="ml-1" />
<span :aria-label="commentsLabel" class="ml-1">
{{ commentsCount }}
</span>
</div>
</div>
</div>
</router-link>
</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';
Vue.use(VueApollo);
const createMockDesign = id => ({
id,
id: Number(id),
image: 'http://via.placeholder.com/300',
name: 'test.jpg',
commentsCount: 2,
......@@ -25,5 +25,20 @@ export default new VueApollo({
createMockDesign(_.uniqueId()),
],
},
resolvers: {
Mutation: {
uploadDesign(ctx, { name }, { cache }) {
const design = {
...createMockDesign(_.uniqueId()),
name,
commentsCount: 0,
};
cache.writeData({ data: design });
return design;
},
},
},
}),
});
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import DesignList from '../components/list/index.vue';
import UploadForm from '../components/upload/form.vue';
import allDesignsQuery from '../queries/allDesigns.graphql';
import uploadDesignQuery from '../queries/uploadDesign.graphql';
export default {
components: {
GlLoadingIcon,
DesignList,
UploadForm,
},
apollo: {
designs: {
......@@ -20,6 +25,7 @@ export default {
return {
designs: [],
error: false,
isSaving: false,
};
},
computed: {
......@@ -27,13 +33,56 @@ export default {
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>
<template>
<div>
<upload-form :is-saving="isSaving" @upload="onUploadDesign" />
<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">
{{ __('An error occurred while loading designs. Please try again.') }}
</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`] = `
>
<design-stub
commentscount="2"
id="1"
image="test"
name="test"
updatedat="01-01-2019"
......@@ -19,6 +20,7 @@ exports[`Design management list component renders list 1`] = `
>
<design-stub
commentscount="2"
id="2"
image="test"
name="test"
updatedat="01-01-2019"
......
// 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`] = `
<div
class="card js-design-list-item"
id="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 design-img"
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"
class="card-footer d-flex w-100"
>
<div
class="d-flex flex-column"
class="d-flex flex-column str-truncated-100"
>
<span
class="bold"
class="bold str-truncated-100"
>
test
</span>
<span>
<span
class="str-truncated-100"
>
Updated
<timeago-stub
cssclass=""
......@@ -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"
>
<icon-stub
class="ml-1"
cssclasses=""
name="comments"
size="16"
......@@ -57,38 +107,41 @@ exports[`Design management list item component renders item with multiple commen
</span>
</div>
</div>
</div>
</router-link-stub>
`;
exports[`Design management list item component renders item with single comment 1`] = `
<div
class="card js-design-list-item"
id="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 design-img"
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"
class="card-footer d-flex w-100"
>
<div
class="d-flex flex-column"
class="d-flex flex-column str-truncated-100"
>
<span
class="bold"
class="bold str-truncated-100"
>
test
</span>
<span>
<span
class="str-truncated-100"
>
Updated
<timeago-stub
cssclass=""
......@@ -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"
>
<icon-stub
class="ml-1"
cssclasses=""
name="comments"
size="16"
......@@ -117,5 +171,5 @@ exports[`Design management list item component renders item with single comment
</span>
</div>
</div>
</div>
</router-link-stub>
`;
......@@ -4,7 +4,7 @@ import Item from 'ee/design_management/components/list/item.vue';
describe('Design management list item component', () => {
let vm;
function createComponent(commentsCount) {
function createComponent(commentsCount = 1) {
vm = shallowMount(Item, {
propsData: {
id: 1,
......@@ -13,6 +13,7 @@ describe('Design management list item component', () => {
commentsCount,
updatedAt: '01-01-2019',
},
stubs: ['router-link'],
});
}
......@@ -21,7 +22,7 @@ describe('Design management list item component', () => {
});
it('renders item with single comment', () => {
createComponent(1);
createComponent();
expect(vm.element).toMatchSnapshot();
});
......@@ -31,4 +32,10 @@ describe('Design management list item component', () => {
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 @@
exports[`Design management index page designs renders designs list 1`] = `
<div>
<uploadform-stub />
<div
class="mt-4"
>
......@@ -14,6 +16,8 @@ exports[`Design management index page designs renders designs list 1`] = `
exports[`Design management index page designs renders empty text 1`] = `
<div>
<uploadform-stub />
<div
class="mt-4"
>
......@@ -26,6 +30,8 @@ exports[`Design management index page designs renders empty text 1`] = `
exports[`Design management index page designs renders error 1`] = `
<div>
<uploadform-stub />
<div
class="mt-4"
>
......@@ -42,13 +48,15 @@ exports[`Design management index page designs renders error 1`] = `
exports[`Design management index page designs renders loading icon 1`] = `
<div>
<uploadform-stub />
<div
class="mt-4"
>
<glloadingicon-stub
color="orange"
label="Loading"
size="2"
size="md"
/>
</div>
</div>
......
import { shallowMount } from '@vue/test-utils';
import Index from 'ee/design_management/pages/index.vue';
import uploadDesignQuery from 'ee/design_management/queries/uploadDesign.graphql';
describe('Design management index page', () => {
const mutate = jest.fn(() => Promise.resolve());
let vm;
function createComponent(loading = false) {
......@@ -11,6 +13,7 @@ describe('Design management index page', () => {
loading,
},
},
mutate,
};
vm = shallowMount(Index, {
......@@ -47,4 +50,53 @@ describe('Design management index page', () => {
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 ""
msgid "Description:"
msgstr ""
msgid "DesignManagement|Error uploading a new design. Please try again"
msgstr ""
msgid "DesignManagement|Upload new designs"
msgstr ""
msgid "Designs"
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