Merge pull request #605 from Human-Connection/399-user-profile-image-uploads

User profile image uploads
This commit is contained in:
mattwr18 2019-05-27 09:32:04 -03:00 committed by GitHub
commit a21fb27351
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 328 additions and 4 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -0,0 +1,34 @@
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
/* global cy */
When('I visit my profile page', () => {
cy.openPage('profile/peter-pan')
})
Then('I should be able to change my profile picture', () => {
const avatarUpload = 'onourjourney.png'
cy.fixture(avatarUpload, 'base64').then(fileContent => {
cy.get('#customdropzone').upload(
{ fileContent, fileName: avatarUpload, mimeType: 'image/png' },
{ subjectType: 'drag-n-drop' },
)
})
cy.get('#customdropzone')
.should('have.attr', 'style')
.and('contains', 'onourjourney')
cy.contains('.iziToast-message', 'Upload successful')
.should('have.length', 1)
})
When("I visit another user's profile page", () => {
cy.openPage('profile/peter-pan')
})
Then('I cannot upload a picture', () => {
cy.get('.ds-card-content')
.children()
.should('not.have.id', 'customdropzone')
.should('have.class', 'ds-avatar')
})

View File

@ -45,6 +45,7 @@ When('people visit my profile page', url => {
cy.openPage('/profile/peter-pan')
})
When('they can see the text in the info box below my avatar', () => {
cy.contains(aboutMeText)
})

View File

@ -0,0 +1,18 @@
Feature: Upload UserProfile Image
As a user
I would like to be able to add an avatar/profile pic to my profile
So that I can personalize my profile
Background:
Given I have a user account
Scenario: Change my UserProfile Image
Given I am logged in
And I visit my profile page
Then I should be able to change my profile picture
Scenario: Unable to change another user's avatar
Given I am logged in with a "user" role
And I visit another user's profile page
Then I cannot upload a picture

View File

@ -13,7 +13,7 @@
// Cypress.Commands.add('login', (email, password) => { ... })
/* globals Cypress cy */
import 'cypress-file-upload'
import { getLangByName } from './helpers'
import users from '../fixtures/users.json'

View File

@ -23,6 +23,7 @@
"cross-env": "^5.2.0",
"cypress": "^3.3.1",
"cypress-cucumber-preprocessor": "^1.11.2",
"cypress-file-upload": "^3.1.2",
"cypress-plugin-retries": "^1.2.2",
"dotenv": "^8.0.0",
"faker": "^4.1.0",
@ -30,4 +31,4 @@
"neo4j-driver": "^1.7.4",
"npm-run-all": "^4.1.5"
}
}
}

View File

@ -0,0 +1,153 @@
<template>
<div>
<vue-dropzone
id="customdropzone"
:key="user.avatar"
ref="el"
:options="dropzoneOptions"
:include-styling="false"
:style="backgroundImage"
@vdropzone-thumbnail="thumbnail"
@vdropzone-drop="vddrop"
/>
</div>
</template>
<script>
import vueDropzone from 'nuxt-dropzone'
import gql from 'graphql-tag'
export default {
components: {
vueDropzone,
},
props: {
user: { type: Object, default: null },
},
data() {
return {
dropzoneOptions: {
url: this.vddrop,
maxFilesize: 0.5,
previewTemplate: this.template(),
dictDefaultMessage: '',
},
}
},
computed: {
backgroundImage() {
const { avatar } = this.user || {}
const userAvatar = avatar.startsWith('/') ? avatar.replace('/', '/api/') : avatar
return {
backgroundImage: `url(${userAvatar})`,
}
},
},
methods: {
template() {
return `<div class="dz-preview dz-file-preview">
<div class="dz-image">
<div data-dz-thumbnail-bg></div>
</div>
<div class="dz-details">
<div class="dz-size"><span data-dz-size></span></div>
<div class="dz-filename"><span data-dz-name></span></div>
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
</div>
</div>
`
},
thumbnail(file, dataUrl) {
let j, len, ref, thumbnailElement
if (file.previewElement) {
this.$refs.el.$el.style.backgroundImage = ''
file.previewElement.classList.remove('dz-file-preview')
ref = file.previewElement.querySelectorAll('[data-dz-thumbnail-bg]')
for (j = 0, len = ref.length; j < len; j++) {
thumbnailElement = ref[j]
thumbnailElement.alt = file.name
thumbnailElement.style.backgroundImage = 'url("' + dataUrl + '")'
}
file.previewElement.classList.add('dz-image-preview')
}
},
vddrop(file) {
const avatarUpload = file[0]
this.$apollo
.mutate({
mutation: gql`
mutation($id: ID!, $avatarUpload: Upload) {
UpdateUser(id: $id, avatarUpload: $avatarUpload) {
id
avatar
}
}
`,
variables: {
avatarUpload,
id: this.user.id,
},
})
.then(() => {
this.$toast.success(this.$t('user.avatar.submitted'))
})
.catch(error => this.$toast.error(error.message))
},
},
}
</script>
<style>
#customdropzone {
margin: -60px auto auto;
width: 122px;
min-height: 122px;
background-size: cover;
background-repeat: no-repeat;
border-radius: 50%;
font-family: 'Arial', sans-serif;
letter-spacing: 0.2px;
color: #777;
transition: background-color 0.2s linear;
padding: 40px;
}
#customdropzone:hover {
cursor: pointer;
}
#customdropzone .dz-preview {
width: 160px;
display: flex;
}
#customdropzone .dz-preview .dz-image {
position: relative;
width: 122px;
height: 122px;
margin: -35px;
}
#customdropzone .dz-preview .dz-image > div {
width: inherit;
height: inherit;
border-radius: 50%;
background-size: cover;
}
#customdropzone .dz-preview .dz-image > img {
width: 100%;
}
#customdropzone .dz-preview .dz-details {
color: white;
transition: opacity 0.2s linear;
text-align: center;
}
#customdropzone .dz-success-mark,
.dz-error-mark,
.dz-remove {
display: none;
}
</style>

View File

@ -0,0 +1,71 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Upload from '.'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
describe('Upload', () => {
let wrapper
const mocks = {
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: { UpdateUser: { id: 'upload1', avatar: '/upload/avatar.jpg' } },
})
.mockRejectedValue({
message: 'File upload unsuccessful! Whatcha gonna do?',
}),
},
$toast: {
success: jest.fn(),
error: jest.fn(),
},
}
const propsData = {
user: {
avatar: '/api/generic.jpg',
},
}
const file = {
filename: 'avatar.jpg',
previewElement: {
classList: {
remove: jest.fn(),
add: jest.fn(),
},
querySelectorAll: jest.fn().mockReturnValue([
{
alt: '',
style: {
'background-image': '/api/generic.jpg',
},
},
]),
},
}
const dataUrl = 'avatar.jpg'
beforeEach(() => {
jest.useFakeTimers()
wrapper = shallowMount(Upload, { localVue, propsData, mocks })
})
it('sends a the UpdateUser mutation when vddrop is called', () => {
wrapper.vm.vddrop([{ filename: 'avatar.jpg' }])
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it('thumbnail', () => {
wrapper.vm.thumbnail(file, dataUrl)
expect(file.previewElement.classList.add).toHaveBeenCalledTimes(1)
})
})

View File

@ -247,5 +247,10 @@
},
"shoutButton": {
"shouted": "shouted"
},
"user": {
"avatar": {
"submitted": "Upload successful"
}
}
}

View File

@ -67,6 +67,7 @@
"jsonwebtoken": "~8.5.1",
"linkify-it": "~2.1.0",
"nuxt": "~2.7.1",
"nuxt-dropzone": "^1.0.2",
"nuxt-env": "~0.1.0",
"stack-utils": "^1.0.2",
"string-hash": "^1.1.3",
@ -76,6 +77,7 @@
"vue-count-to": "~1.0.13",
"vue-izitoast": "1.1.2",
"vue-sweetalert-icons": "~3.2.0",
"vue2-dropzone": "^3.5.9",
"vuex-i18n": "~1.11.0",
"zxcvbn": "^4.4.2"
},
@ -111,4 +113,4 @@
"vue-jest": "~3.0.4",
"vue-svg-loader": "~0.12.0"
}
}
}

View File

@ -14,7 +14,12 @@
:class="{'disabled-content': user.disabled}"
style="position: relative; height: auto;"
>
<hc-upload
v-if="myProfile"
:user="user"
/>
<ds-avatar
v-else
:image="user.avatar"
:name="userName"
class="profile-avatar"
@ -223,7 +228,7 @@
>
<a :href="link.url">
<ds-avatar :image="link.favicon" />
{{ link.username }}
{{ 'link.username' }}
</a>
</ds-space>
</template>
@ -327,6 +332,7 @@ import HcBadges from '~/components/Badges.vue'
import HcLoadMore from '~/components/LoadMore.vue'
import HcEmpty from '~/components/Empty.vue'
import ContentMenu from '~/components/ContentMenu'
import HcUpload from '~/components/Upload'
export default {
components: {
@ -338,6 +344,7 @@ export default {
HcLoadMore,
HcEmpty,
ContentMenu,
HcUpload,
},
transition: {
name: 'slide-up',

View File

@ -1,5 +1,6 @@
export default ({ app }) => {
const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000'
return {
httpEndpoint: process.server ? backendUrl : '/api',
httpLinkOptions: {

View File

@ -3968,6 +3968,11 @@ dotenv@^6.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
dropzone@^5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-5.5.1.tgz#06e2f513e61d6aa363d4b556f18574f47cf7ba26"
integrity sha512-3VduRWLxx9hbVr42QieQN25mx/I61/mRdUSuxAmDGdDqZIN8qtP7tcKMa3KfpJjuGjOJGYYUzzeq6eGDnkzesA==
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@ -7579,6 +7584,13 @@ number-is-nan@^1.0.0:
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
nuxt-dropzone@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nuxt-dropzone/-/nuxt-dropzone-1.0.2.tgz#7b39014ebf4c2084ea5c976f8d9f7b3cead2c7af"
integrity sha512-Oj6YrQxNH5KhCyFSFz2O809u23+cFAevBTdcld88qakbR2l5stTQjrv8VJ9beaqfenT9kKEkhYQT0mXc3nUdKw==
dependencies:
vue2-dropzone "3.5.8"
nuxt-env@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nuxt-env/-/nuxt-env-0.1.0.tgz#8ac50b9ff45391ad3044ea932cbd05f06a585f87"
@ -11155,6 +11167,20 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue2-dropzone@3.5.8:
version "3.5.8"
resolved "https://registry.yarnpkg.com/vue2-dropzone/-/vue2-dropzone-3.5.8.tgz#cbe92d5424b5cc62c4d4ad62814d0cf6f3bb6cda"
integrity sha512-32rLGSx+mLKhyzxRz4CdeNT9JmbO6NsYX8m83WYqrf2ilRbm6KSZmUqZ8EIT+2dwq8EzY9jdrWlWuZJRBFPUGw==
dependencies:
dropzone "^5.5.1"
vue2-dropzone@^3.5.9:
version "3.5.9"
resolved "https://registry.yarnpkg.com/vue2-dropzone/-/vue2-dropzone-3.5.9.tgz#a63999a45a7aad24d4c21e3d35be409b4e6bdce8"
integrity sha512-nJz6teulVKlZIAeKgvPU7wBI/gzfIgqDOrEp1okSkQIkdprDVCoM0U7XWM0NOM4AAVX+4XGRtMoocYWdTYb3bQ==
dependencies:
dropzone "^5.5.1"
vue@^2.6.10, vue@^2.6.6:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"

View File

@ -1815,6 +1815,11 @@ cypress-cucumber-preprocessor@^1.11.2:
glob "^7.1.2"
through "^2.3.8"
cypress-file-upload@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-3.1.2.tgz#4a0024f99ca157565bf2b20c110e6e6874da28cb"
integrity sha512-gZE2G7ZTD2Y8APrcgs+ATRMKs/IgH2rafCmi+8o99q5sDoNRLR+XKxOcoyWLehj9raGnO98YDYO8DY7k1VMGBw==
cypress-plugin-retries@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/cypress-plugin-retries/-/cypress-plugin-retries-1.2.2.tgz#7235371ca575ad9e16f883169e7f1588379f80f2"