Merge branch 'master' of github.com:Human-Connection/Human-Connection into 296-image_component

This commit is contained in:
aonomike 2019-05-10 14:49:02 +03:00
commit cc61ce5461
32 changed files with 1391 additions and 1120 deletions

View File

@ -15,7 +15,7 @@
"test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand", "test:jest:cmd": "wait-on tcp:4001 tcp:4123 && jest --forceExit --detectOpenHandles --runInBand",
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/", "test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
"test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand", "test:jest:cmd:debug": "wait-on tcp:4001 tcp:4123 && node --inspect-brk ./node_modules/.bin/jest -i --forceExit --detectOpenHandles --runInBand",
"test:jest": "run-p --race test:before:* 'test:jest:cmd {@}' --", "test:jest": "run-p --race test:before:* \"test:jest:cmd {@}\" --",
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --", "test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
"test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --", "test:jest:debug": "run-p --race test:before:* 'test:jest:cmd:debug {@}' --",
"db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js", "db:script:seed": "wait-on tcp:4001 && babel-node src/seed/seed-db.js",

View File

@ -75,6 +75,7 @@ const permissions = shield({
DeleteBadge: isAdmin, DeleteBadge: isAdmin,
AddUserBadges: isAdmin, AddUserBadges: isAdmin,
CreateSocialMedia: isAuthenticated, CreateSocialMedia: isAuthenticated,
DeleteSocialMedia: isAuthenticated,
// AddBadgeRewarded: isAdmin, // AddBadgeRewarded: isAdmin,
// RemoveBadgeRewarded: isAdmin, // RemoveBadgeRewarded: isAdmin,
reward: isAdmin, reward: isAdmin,

View File

@ -40,10 +40,13 @@ describe('authorization', () => {
}) })
it('does not expose the owner\'s email address', async () => { it('does not expose the owner\'s email address', async () => {
let response = {}
try { try {
await action() await action()
} catch (error) { } catch (error) {
expect(error.response.data).toEqual({ User: [ { email: null } ] }) response = error.response.data
} finally {
expect(response).toEqual({ User: [ null ] })
} }
}) })
}) })
@ -74,11 +77,13 @@ describe('authorization', () => {
}) })
it('does not expose the owner\'s email address', async () => { it('does not expose the owner\'s email address', async () => {
let response
try { try {
await action() await action()
} catch (error) { } catch (error) {
expect(error.response.data).toEqual({ User: [ { email: null } ] }) response = error.response.data
} }
expect(response).toEqual({ User: [ null ] })
}) })
}) })
}) })

View File

@ -1,6 +1,6 @@
import slugify from 'slug' import slugify from 'slug'
export default async function uniqueSlug (string, isUnique) { export default async function uniqueSlug (string, isUnique) {
let slug = slugify(string, { let slug = slugify(string || 'anonymous', {
lower: true lower: true
}) })
if (await isUnique(slug)) return slug if (await isUnique(slug)) return slug

View File

@ -15,4 +15,11 @@ describe('uniqueSlug', () => {
.mockResolvedValueOnce(true) .mockResolvedValueOnce(true)
expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1') expect(uniqueSlug(string, isUnique)).resolves.toEqual('hello-world-1')
}) })
it('slugify null string', () => {
const string = null
const isUnique = jest.fn()
.mockResolvedValue(true)
expect(uniqueSlug(string, isUnique)).resolves.toEqual('anonymous')
})
}) })

View File

@ -77,7 +77,7 @@ describe('slugify', () => {
describe('CreateUser', () => { describe('CreateUser', () => {
const action = async (mutation, params) => { const action = async (mutation, params) => {
return authenticatedClient.request(`mutation { return authenticatedClient.request(`mutation {
${mutation}(password: "yo", ${params}) { slug } ${mutation}(password: "yo", email: "123@123.de", ${params}) { slug }
}`) }`)
} }
it('generates a slug based on name', async () => { it('generates a slug based on name', async () => {

View File

@ -1,5 +1,7 @@
import createOrUpdateLocations from './nodes/locations'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { UserInputError } from 'apollo-server'
import createOrUpdateLocations from './nodes/locations'
dotenv.config() dotenv.config()
@ -11,6 +13,10 @@ export default {
return result return result
}, },
UpdateUser: async (resolve, root, args, context, info) => { UpdateUser: async (resolve, root, args, context, info) => {
const USERNAME_MIN_LENGTH = 3 // TODO move to the correct place
if (!args.name || args.name.length < USERNAME_MIN_LENGTH) {
throw new UserInputError(`Username must be at least ${USERNAME_MIN_LENGTH} characters long!`)
}
const result = await resolve(root, args, context, info) const result = await resolve(root, args, context, info)
await createOrUpdateLocations(args.id, args.locationName, context.driver) await createOrUpdateLocations(args.id, args.locationName, context.driver)
return result return result

View File

@ -0,0 +1,81 @@
import { GraphQLClient } from 'graphql-request'
import { host } from '../jest/helpers'
import Factory from '../seed/factories'
const factory = Factory()
let client
afterAll(async () => {
await factory.cleanDatabase()
})
describe('userMiddleware', () => {
describe('create User', () => {
const mutation = `
mutation($id: ID, $password: String!, $email: String!) {
CreateUser(id: $id, password: $password, email: $email) {
id
}
}
`
client = new GraphQLClient(host)
it('with password and email', async () => {
const variables = {
password: '123',
email: '123@123.de'
}
const expected = {
CreateUser: {
id: expect.any(String)
}
}
await expect(client.request(mutation, variables))
.resolves.toEqual(expected)
})
})
describe('update User', () => {
const mutation = `
mutation($id: ID!, $name: String) {
UpdateUser(id: $id, name: $name) {
name
}
}
`
client = new GraphQLClient(host)
// TODO why is this failing - it returns { UpdateUser: null } - that should not be
/* it('name within specifications', async () => {
const variables = {
id: 'u1',
name: 'Peter Lustig'
}
const expected = {
UpdateUser: {
name: 'Peter Lustig'
}
}
await expect(client.request(mutation, variables))
.resolves.toEqual(expected)
}) */
it('with no name', async () => {
const variables = {
id: 'u1'
}
const expected = 'Username must be at least 3 characters long!'
await expect(client.request(mutation, variables))
.rejects.toThrow(expected)
})
it('with too short name', async () => {
const variables = {
id: 'u1'
}
const expected = 'Username must be at least 3 characters long!'
await expect(client.request(mutation, variables))
.rejects.toThrow(expected)
})
})
})

View File

@ -3,6 +3,9 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
export default { export default {
Mutation: { Mutation: {
CreateSocialMedia: async (object, params, context, resolveInfo) => { CreateSocialMedia: async (object, params, context, resolveInfo) => {
/**
* TODO?: Creates double Nodes!
*/
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false) const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
const session = context.driver.session() const session = context.driver.session()
await session.run( await session.run(
@ -15,6 +18,11 @@ export default {
) )
session.close() session.close()
return socialMedia
},
DeleteSocialMedia: async (object, params, context, resolveInfo) => {
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
return socialMedia return socialMedia
} }
} }

View File

@ -7,9 +7,18 @@ const factory = Factory()
describe('CreateSocialMedia', () => { describe('CreateSocialMedia', () => {
let client let client
let headers let headers
const mutation = ` const mutationC = `
mutation($url: String!) { mutation($url: String!) {
CreateSocialMedia(url: $url) { CreateSocialMedia(url: $url) {
id
url
}
}
`
const mutationD = `
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
id
url url
} }
} }
@ -30,20 +39,63 @@ describe('CreateSocialMedia', () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
const variables = { url: 'http://nsosp.org' }
await expect(
client.request(mutationC, variables)
).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => { describe('authenticated', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ email: 'test@example.org', password: '1234' }) headers = await login({ email: 'test@example.org', password: '1234' })
client = new GraphQLClient(host, { headers }) client = new GraphQLClient(host, { headers })
}) })
it('creates social media with correct URL', async () => {
const variables = { url: 'http://nsosp.org' }
await expect(
client.request(mutationC, variables)
).resolves.toEqual(expect.objectContaining({
CreateSocialMedia: {
id: expect.any(String),
url: 'http://nsosp.org'
}
}))
})
it('deletes social media', async () => {
const creationVariables = { url: 'http://nsosp.org' }
const { CreateSocialMedia } = await client.request(mutationC, creationVariables)
const { id } = CreateSocialMedia
const deletionVariables = { id }
const expected = {
DeleteSocialMedia: {
id: id,
url: 'http://nsosp.org'
}
}
await expect(
client.request(mutationD, deletionVariables)
).resolves.toEqual(expected)
})
it('rejects empty string', async () => { it('rejects empty string', async () => {
const variables = { url: '' } const variables = { url: '' }
await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL') await expect(
client.request(mutationC, variables)
).rejects.toThrow('Input is not a URL')
}) })
it('validates URLs', async () => { it('validates URLs', async () => {
const variables = { url: 'not-a-url' } const variables = { url: 'not-a-url' }
await expect(client.request(mutation, variables)).rejects.toThrow('Input is not a URL') await expect(
client.request(mutationC, variables)
).rejects.toThrow('Input is not a URL')
}) })
}) })
}) })

View File

@ -339,7 +339,7 @@ describe('do not expose private RSA key', () => {
email: 'apfel-strudel@test.org' email: 'apfel-strudel@test.org'
} }
await client.request(gql` await client.request(gql`
mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String) { mutation($id: ID, $password: String!, $slug: String, $name: String, $email: String!) {
CreateUser(id: $id, password: $password, slug: $slug, name: $name, email: $email) { CreateUser(id: $id, password: $password, slug: $slug, name: $name, email: $email) {
id id
} }

View File

@ -95,7 +95,7 @@ type User {
id: ID! id: ID!
actorId: String actorId: String
name: String name: String
email: String email: String!
slug: String slug: String
password: String! password: String!
avatar: String avatar: String

View File

@ -20,7 +20,7 @@ export default function create (params) {
$name: String $name: String
$slug: String $slug: String
$password: String! $password: String!
$email: String $email: String!
$avatar: String $avatar: String
$about: String $about: String
$role: UserGroupEnum $role: UserGroupEnum

View File

@ -77,7 +77,7 @@ Then('I should be on the {string} page', page => {
.should('contain', 'Social media') .should('contain', 'Social media')
}) })
Then('I add a social media link', () => { When('I add a social media link', () => {
cy.get("input[name='social-media']") cy.get("input[name='social-media']")
.type('https://freeradical.zone/peter-pan') .type('https://freeradical.zone/peter-pan')
.get('button') .get('button')
@ -87,7 +87,7 @@ Then('I add a social media link', () => {
Then('it gets saved successfully', () => { Then('it gets saved successfully', () => {
cy.get('.iziToast-message') cy.get('.iziToast-message')
.should('contain', 'Updated user') .should('contain', 'Added social media')
}) })
Then('the new social media link shows up on the page', () => { Then('the new social media link shows up on the page', () => {
@ -110,3 +110,13 @@ Then('they should be able to see my social media links', () => {
.get('a[href="https://freeradical.zone/peter-pan"]') .get('a[href="https://freeradical.zone/peter-pan"]')
.should('have.length', 1) .should('have.length', 1)
}) })
When('I delete a social media link', () => {
cy.get("a[name='delete']")
.click()
})
Then('it gets deleted successfully', () => {
cy.get('.iziToast-message')
.should('contain', 'Deleted social media')
})

View File

@ -19,3 +19,11 @@ Feature: List Social Media Accounts
Given I have added a social media link Given I have added a social media link
When people visit my profile page When people visit my profile page
Then they should be able to see my social media links Then they should be able to see my social media links
Scenario: Deleting Social Media
Given I am on the "settings" page
And I click on the "Social media" link
Then I should be on the "/settings/my-social-media" page
Given I have added a social media link
When I delete a social media link
Then it gets deleted successfully

View File

@ -19,7 +19,7 @@
"test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov" "test:jest": "cd webapp && yarn test && cd ../backend && yarn test:jest && codecov"
}, },
"devDependencies": { "devDependencies": {
"codecov": "^3.3.0", "codecov": "^3.4.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"cypress": "^3.2.0", "cypress": "^3.2.0",
"cypress-cucumber-preprocessor": "^1.11.0", "cypress-cucumber-preprocessor": "^1.11.0",

View File

@ -51,8 +51,8 @@ describe('User', () => {
it('renders anonymous user', () => { it('renders anonymous user', () => {
const wrapper = Wrapper() const wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('Tilda Swinton') expect(wrapper.text()).toBe('')
expect(wrapper.text()).toMatch('Anonymus') expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym')
}) })
describe('given an user', () => { describe('given an user', () => {
@ -65,7 +65,7 @@ describe('User', () => {
it('renders user name', () => { it('renders user name', () => {
const wrapper = Wrapper() const wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('Anonymous') expect(mocks.$t).not.toHaveBeenCalledWith('profile.userAnonym')
expect(wrapper.text()).toMatch('Tilda Swinton') expect(wrapper.text()).toMatch('Tilda Swinton')
}) })
@ -77,7 +77,7 @@ describe('User', () => {
it('renders anonymous user', () => { it('renders anonymous user', () => {
const wrapper = Wrapper() const wrapper = Wrapper()
expect(wrapper.text()).not.toMatch('Tilda Swinton') expect(wrapper.text()).not.toMatch('Tilda Swinton')
expect(wrapper.text()).toMatch('Anonymus') expect(mocks.$t).toHaveBeenCalledWith('profile.userAnonym')
}) })
describe('current user is a moderator', () => { describe('current user is a moderator', () => {

View File

@ -12,7 +12,7 @@
<b <b
class="username" class="username"
style="vertical-align: middle;" style="vertical-align: middle;"
>Anonymus</b> >{{ $t('profile.userAnonym') }}</b>
</div> </div>
</div> </div>
<dropdown <dropdown
@ -38,7 +38,7 @@
> >
<ds-avatar <ds-avatar
:image="user.avatar" :image="user.avatar"
:name="user.name" :name="userName"
style="display: inline-block; vertical-align: middle;" style="display: inline-block; vertical-align: middle;"
size="32px" size="32px"
/> />
@ -47,7 +47,7 @@
<b <b
class="username" class="username"
style="vertical-align: middle;" style="vertical-align: middle;"
>{{ user.name | truncate(trunc, 18) }}</b> >{{ userName | truncate(18) }}</b>
</div> </div>
<!-- Time --> <!-- Time -->
<div <div
@ -141,8 +141,8 @@
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import HcRelativeDateTime from '~/components/RelativeDateTime' import HcRelativeDateTime from '~/components/RelativeDateTime'
import HcFollowButton from '~/components/FollowButton.vue' import HcFollowButton from '~/components/FollowButton'
import HcBadges from '~/components/Badges.vue' import HcBadges from '~/components/Badges'
import Dropdown from '~/components/Dropdown' import Dropdown from '~/components/Dropdown'
export default { export default {
@ -173,6 +173,10 @@ export default {
const { id, slug } = this.user const { id, slug } = this.user
if (!(id && slug)) return '' if (!(id && slug)) return ''
return { name: 'profile-id-slug', params: { slug, id } } return { name: 'profile-id-slug', params: { slug, id } }
},
userName() {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
} }
} }
} }

View File

@ -61,7 +61,8 @@
slot-scope="{closeMenu}" slot-scope="{closeMenu}"
> >
<div class="avatar-menu-popover"> <div class="avatar-menu-popover">
{{ $t('login.hello') }} <b>{{ user.name }}</b> {{ $t('login.hello') }}
<b>{{ userName }}</b>
<template v-if="user.role !== 'user'"> <template v-if="user.role !== 'user'">
<ds-text <ds-text
color="softer" color="softer"
@ -83,7 +84,8 @@
:parents="item.parents" :parents="item.parents"
@click.native="closeMenu(false)" @click.native="closeMenu(false)"
> >
<ds-icon :name="item.route.icon" /> {{ item.route.name }} <ds-icon :name="item.route.icon" />
{{ item.route.name }}
</ds-menu-item> </ds-menu-item>
</ds-menu> </ds-menu>
<hr> <hr>
@ -91,7 +93,8 @@
class="logout-link" class="logout-link"
:to="{ name: 'logout'}" :to="{ name: 'logout'}"
> >
<ds-icon name="sign-out" /> {{ $t('login.logout') }} <ds-icon name="sign-out" />
{{ $t('login.logout') }}
</nuxt-link> </nuxt-link>
</div> </div>
</template> </template>
@ -146,6 +149,10 @@ export default {
quickSearchResults: 'search/quickResults', quickSearchResults: 'search/quickResults',
quickSearchPending: 'search/quickPending' quickSearchPending: 'search/quickPending'
}), }),
userName() {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
},
routes() { routes() {
if (!this.user.slug) { if (!this.user.slug) {
return [] return []

View File

@ -21,6 +21,7 @@
"following": "Folgt", "following": "Folgt",
"shouted": "Empfohlen", "shouted": "Empfohlen",
"commented": "Kommentiert", "commented": "Kommentiert",
"userAnonym": "Anonymus",
"socialMedia": "Wo sonst finde ich" "socialMedia": "Wo sonst finde ich"
}, },
"notifications": { "notifications": {
@ -38,8 +39,10 @@
"data": { "data": {
"name": "Deine Daten", "name": "Deine Daten",
"labelName": "Dein Name", "labelName": "Dein Name",
"namePlaceholder": "Petra Lustig",
"labelCity": "Deine Stadt oder Region", "labelCity": "Deine Stadt oder Region",
"labelBio": "Über dich" "labelBio": "Über dich",
"success": "Deine Daten wurden erfolgreich aktualisiert!"
}, },
"security": { "security": {
"name": "Sicherheit", "name": "Sicherheit",
@ -65,8 +68,10 @@
}, },
"social-media": { "social-media": {
"name": "Soziale Medien", "name": "Soziale Medien",
"placeholder": "Füge eine Social-Media URL hinzu",
"submit": "Link hinzufügen", "submit": "Link hinzufügen",
"success": "Profil aktualisiert" "successAdd": "Social-Media hinzugefügt. Profil aktualisiert!",
"successDelete": "Social-Media gelöscht. Profil aktualisiert!"
} }
}, },
"admin": { "admin": {

View File

@ -21,6 +21,7 @@
"following": "Following", "following": "Following",
"shouted": "Shouted", "shouted": "Shouted",
"commented": "Commented", "commented": "Commented",
"userAnonym": "Anonymous",
"socialMedia": "Where else can I find" "socialMedia": "Where else can I find"
}, },
"notifications": { "notifications": {
@ -38,8 +39,10 @@
"data": { "data": {
"name": "Your data", "name": "Your data",
"labelName": "Your Name", "labelName": "Your Name",
"namePlaceholder": "Femanon Funny",
"labelCity": "Your City or Region", "labelCity": "Your City or Region",
"labelBio": "About You" "labelBio": "About You",
"success": "Your data was successfully updated!"
}, },
"security": { "security": {
"name": "Security", "name": "Security",
@ -65,8 +68,10 @@
}, },
"social-media": { "social-media": {
"name": "Social media", "name": "Social media",
"placeholder": "Add social media url",
"submit": "Add link", "submit": "Add link",
"success": "Updated user profile" "successAdd": "Added social media. Updated user profile!",
"successDelete": "Deleted social media. Updated user profile!"
} }
}, },
"admin": { "admin": {

View File

@ -15,13 +15,15 @@
"followers": "Seguenti", "followers": "Seguenti",
"following": "Seguendo", "following": "Seguendo",
"shouted": "Gridato", "shouted": "Gridato",
"commented": "Commentato" "commented": "Commentato",
"userAnonym": "Anonymous"
}, },
"settings": { "settings": {
"name": "Impostazioni", "name": "Impostazioni",
"data": { "data": {
"name": "I tuoi dati", "name": "I tuoi dati",
"labelName": "Nome", "labelName": "Nome",
"namePlaceholder": "Anonymous",
"labelCity": "La tua città o regione", "labelCity": "La tua città o regione",
"labelBio": "Su di te" "labelBio": "Su di te"
}, },

View File

@ -15,13 +15,15 @@
"followers": "Obserwujący", "followers": "Obserwujący",
"following": "Obserwowani", "following": "Obserwowani",
"shouted": "Krzyknij", "shouted": "Krzyknij",
"commented": "Skomentuj" "commented": "Skomentuj",
"userAnonym": "Anonymous"
}, },
"settings": { "settings": {
"name": "Ustawienia", "name": "Ustawienia",
"data": { "data": {
"name": "Twoje dane", "name": "Twoje dane",
"labelName": "Twoje dane", "labelName": "Twoje dane",
"namePlaceholder": "Anonymous",
"labelCity": "Twoje miasto lub region", "labelCity": "Twoje miasto lub region",
"labelBio": "O Tobie" "labelBio": "O Tobie"
}, },

View File

@ -15,13 +15,15 @@
"followers": "Seguidores", "followers": "Seguidores",
"following": "Seguindo", "following": "Seguindo",
"shouted": "Aclamou", "shouted": "Aclamou",
"commented": "Comentou" "commented": "Comentou",
"userAnonym": "Anonymous"
}, },
"settings": { "settings": {
"name": "Configurações", "name": "Configurações",
"data": { "data": {
"name": "Seus dados", "name": "Seus dados",
"labelName": "Seu nome", "labelName": "Seu nome",
"namePlaceholder": "Anonymous",
"labelCity": "Sua cidade ou estado", "labelCity": "Sua cidade ou estado",
"labelBio": "Sobre você" "labelBio": "Sobre você"
}, },

View File

@ -46,7 +46,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@human-connection/styleguide": "0.5.15", "@human-connection/styleguide": "0.5.17",
"@nuxtjs/apollo": "4.0.0-rc4", "@nuxtjs/apollo": "4.0.0-rc4",
"@nuxtjs/axios": "~5.4.1", "@nuxtjs/axios": "~5.4.1",
"@nuxtjs/dotenv": "~1.3.0", "@nuxtjs/dotenv": "~1.3.0",
@ -65,8 +65,8 @@
"nuxt-env": "~0.1.0", "nuxt-env": "~0.1.0",
"stack-utils": "^1.0.2", "stack-utils": "^1.0.2",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
"tiptap": "^1.18.0", "tiptap": "1.17.0",
"tiptap-extensions": "^1.18.1", "tiptap-extensions": "1.17.0",
"v-tooltip": "~2.0.2", "v-tooltip": "~2.0.2",
"vue-count-to": "~1.0.13", "vue-count-to": "~1.0.13",
"vue-izitoast": "1.1.2", "vue-izitoast": "1.1.2",
@ -95,7 +95,7 @@
"nodemon": "~1.19.0", "nodemon": "~1.19.0",
"prettier": "~1.14.3", "prettier": "~1.14.3",
"sass-loader": "~7.1.0", "sass-loader": "~7.1.0",
"tippy.js": "^4.3.0", "tippy.js": "^4.3.1",
"vue-jest": "~3.0.4", "vue-jest": "~3.0.4",
"vue-svg-loader": "~0.12.0" "vue-svg-loader": "~0.12.0"
} }

View File

@ -16,7 +16,7 @@
> >
<ds-avatar <ds-avatar
:image="user.avatar" :image="user.avatar"
:name="user.name || 'Anonymus'" :name="userName"
class="profile-avatar" class="profile-avatar"
size="120px" size="120px"
/> />
@ -35,7 +35,7 @@
align="center" align="center"
no-margin no-margin
> >
{{ user.name }} {{ userName }}
</ds-heading> </ds-heading>
<ds-text <ds-text
v-if="user.location" v-if="user.location"
@ -123,7 +123,7 @@
tag="h5" tag="h5"
color="soft" color="soft"
> >
Wem folgt {{ user.name | truncate(15) }}? Wem folgt {{ userName | truncate(15) }}?
</ds-text> </ds-text>
</ds-space> </ds-space>
<template v-if="user.following && user.following.length"> <template v-if="user.following && user.following.length">
@ -154,7 +154,7 @@
</template> </template>
<template v-else> <template v-else>
<p style="text-align: center; opacity: .5;"> <p style="text-align: center; opacity: .5;">
{{ user.name }} folgt niemandem {{ userName }} folgt niemandem
</p> </p>
</template> </template>
</ds-card> </ds-card>
@ -168,7 +168,7 @@
tag="h5" tag="h5"
color="soft" color="soft"
> >
Wer folgt {{ user.name | truncate(15) }}? Wer folgt {{ userName | truncate(15) }}?
</ds-text> </ds-text>
</ds-space> </ds-space>
<template v-if="user.followedBy && user.followedBy.length"> <template v-if="user.followedBy && user.followedBy.length">
@ -199,7 +199,7 @@
</template> </template>
<template v-else> <template v-else>
<p style="text-align: center; opacity: .5;"> <p style="text-align: center; opacity: .5;">
niemand folgt {{ user.name }} niemand folgt {{ userName }}
</p> </p>
</template> </template>
</ds-card> </ds-card>
@ -208,9 +208,7 @@
margin="large" margin="large"
> >
<ds-card style="position: relative; height: auto;"> <ds-card style="position: relative; height: auto;">
<ds-space <ds-space margin="x-small">
margin="x-small"
>
<ds-text <ds-text
tag="h5" tag="h5"
color="soft" color="soft"
@ -224,9 +222,7 @@
margin="x-small" margin="x-small"
> >
<a :href="link.url"> <a :href="link.url">
<ds-avatar <ds-avatar :image="link.favicon" />
:image="link.favicon"
/>
{{ link.username }} {{ link.username }}
</a> </a>
</ds-space> </ds-space>
@ -393,6 +389,10 @@ export default {
const username = url.split('/').pop() const username = url.split('/').pop()
return { url, username, favicon } return { url, username, favicon }
}) })
},
userName() {
const { name } = this.user || {}
return name || this.$t('profile.userAnonym')
} }
}, },
watch: { watch: {

View File

@ -0,0 +1,73 @@
import { mount, createLocalVue } from '@vue/test-utils'
import index from './index.vue'
import Vue from 'vue'
import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Styleguide)
describe('index.vue', () => {
let Wrapper
let store
let mocks
let getters
beforeEach(() => {
mocks = {
$t: jest.fn(),
$apollo: {
mutate: jest
.fn()
.mockRejectedValue({ message: 'Ouch!' })
.mockResolvedValueOnce({
data: {
UpdateUser: {
id: 'u1',
name: 'Peter',
locationName: 'Berlin',
about: 'Smth'
}
}
})
},
$toast: {
error: jest.fn(),
success: jest.fn()
}
}
getters = {
'auth/user': () => {
return {}
}
}
})
describe('mount', () => {
const Wrapper = () => {
store = new Vuex.Store({
getters
})
return mount(index, { store, mocks, localVue })
}
it('renders', () => {
expect(Wrapper().contains('div')).toBe(true)
})
describe('given a new username and hitting submit', () => {
it('calls updateUser mutation', () => {
const wrapper = Wrapper()
const input = wrapper.find('#name')
const submitForm = wrapper.find('.ds-form')
input.setValue('Peter')
submitForm.trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalled()
})
})
})
})

View File

@ -9,7 +9,7 @@
model="name" model="name"
icon="user" icon="user"
:label="$t('settings.data.labelName')" :label="$t('settings.data.labelName')"
:placeholder="$t('settings.data.labelName')" :placeholder="$t('settings.data.namePlaceholder')"
/> />
<!-- eslint-disable vue/use-v-on-exact --> <!-- eslint-disable vue/use-v-on-exact -->
<ds-select <ds-select
@ -19,7 +19,7 @@
:options="cities" :options="cities"
:label="$t('settings.data.labelCity')" :label="$t('settings.data.labelCity')"
:placeholder="$t('settings.data.labelCity')" :placeholder="$t('settings.data.labelCity')"
:loading="loading" :loading="loadingGeo"
@input.native="handleCityInput" @input.native="handleCityInput"
/> />
<!-- eslint-enable vue/use-v-on-exact --> <!-- eslint-enable vue/use-v-on-exact -->
@ -36,7 +36,7 @@
style="float: right;" style="float: right;"
icon="check" icon="check"
type="submit" type="submit"
:loading="sending" :loading="loadingData"
primary primary
> >
{{ $t('actions.save') }} {{ $t('actions.save') }}
@ -88,8 +88,8 @@ export default {
return { return {
axiosSource: null, axiosSource: null,
cities: [], cities: [],
sending: false, loadingData: false,
loading: false, loadingGeo: false,
formData: {} formData: {}
} }
}, },
@ -111,13 +111,13 @@ export default {
...mapMutations({ ...mapMutations({
setCurrentUser: 'auth/SET_USER' setCurrentUser: 'auth/SET_USER'
}), }),
submit() { async submit() {
this.sending = true this.loadingData = true
const { name, about } = this.formData const { name, about } = this.formData
let { locationName } = this.formData let { locationName } = this.formData
locationName = locationName && (locationName['label'] || locationName) locationName = locationName && (locationName['label'] || locationName)
this.$apollo try {
.mutate({ const { data } = await this.$apollo.mutate({
mutation, mutation,
variables: { variables: {
id: this.currentUser.id, id: this.currentUser.id,
@ -135,15 +135,12 @@ export default {
}) })
} }
}) })
.then(data => { this.$toast.success(this.$t('settings.data.success'))
this.$toast.success('Updated user') } catch (err) {
}) this.$toast.error(err.message)
.catch(err => { } finally {
this.$toast.error(err.message) this.loadingData = false
}) }
.finally(() => {
this.sending = false
})
}, },
handleCityInput(value) { handleCityInput(value) {
clearTimeout(timeout) clearTimeout(timeout)
@ -181,7 +178,7 @@ export default {
return return
} }
this.loading = true this.loadingGeo = true
this.axiosSource = CancelToken.source() this.axiosSource = CancelToken.source()
const place = encodeURIComponent(value) const place = encodeURIComponent(value)
@ -198,7 +195,7 @@ export default {
this.cities = this.processCityResults(res) this.cities = this.processCityResults(res)
}) })
.finally(() => { .finally(() => {
this.loading = false this.loadingGeo = false
}) })
} }
} }

View File

@ -71,6 +71,40 @@ describe('my-social-media.vue', () => {
const socialMediaLink = wrapper.find('a').attributes().href const socialMediaLink = wrapper.find('a').attributes().href
expect(socialMediaLink).toBe(socialMediaUrl) expect(socialMediaLink).toBe(socialMediaUrl)
}) })
beforeEach(() => {
mocks = {
$t: jest.fn(),
$apollo: {
mutate: jest
.fn()
.mockRejectedValue({ message: 'Ouch!' })
.mockResolvedValueOnce({
data: { DeleteSocialMeda: { id: 's1', url: socialMediaUrl } }
})
},
$toast: {
error: jest.fn(),
success: jest.fn()
}
}
getters = {
'auth/user': () => {
return {
socialMedia: [{ id: 's1', url: socialMediaUrl }]
}
}
}
})
it('displays a trash sympol after a social media and allows the user to delete it', () => {
wrapper = Wrapper()
const deleteSelector = wrapper.find({ name: 'delete' })
expect(deleteSelector).toEqual({ selector: 'Component' })
const icon = wrapper.find({ name: 'trash' })
icon.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
}) })
describe('currentUser does not have a social media account linked', () => { describe('currentUser does not have a social media account linked', () => {

View File

@ -8,7 +8,7 @@
<ds-list> <ds-list>
<ds-list-item <ds-list-item
v-for="link in socialMediaLinks" v-for="link in socialMediaLinks"
:key="link.url" :key="link.id"
> >
<a :href="link.url"> <a :href="link.url">
<hc-image <hc-image
@ -19,26 +19,39 @@
/> />
{{ link.url }} {{ link.url }}
</a> </a>
&nbsp;&nbsp; <span class="layout-leave-active">|</span> &nbsp;&nbsp;
<ds-icon
name="edit"
class="layout-leave-active"
/>
<a
name="delete"
@click="handleDeleteSocialMedia(link)"
>
<ds-icon name="trash" />
</a>
</ds-list-item> </ds-list-item>
</ds-list> </ds-list>
</ds-space> </ds-space>
<div>
<ds-input
v-model="value"
placeholder="Add social media url"
name="social-media"
:schema="{type: 'url'}"
/>
</div>
<ds-space margin-top="base"> <ds-space margin-top="base">
<div> <div>
<ds-button <ds-input
primary v-model="value"
@click="handleAddSocialMedia" :placeholder="$t('settings.social-media.placeholder')"
> name="social-media"
{{ $t('settings.social-media.submit') }} :schema="{type: 'url'}"
</ds-button> />
</div> </div>
<ds-space margin-top="base">
<div>
<ds-button
primary
@click="handleAddSocialMedia"
>
{{ $t('settings.social-media.submit') }}
</ds-button>
</div>
</ds-space>
</ds-space> </ds-space>
</ds-card> </ds-card>
</template> </template>
@ -63,13 +76,13 @@ export default {
socialMediaLinks() { socialMediaLinks() {
const { socialMedia = [] } = this.currentUser const { socialMedia = [] } = this.currentUser
return socialMedia.map(socialMedia => { return socialMedia.map(socialMedia => {
const { url } = socialMedia const { id, url } = socialMedia
const matches = url.match( const matches = url.match(
/^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:\/\n?]+)/g /^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:\/\n?]+)/g
) )
const [domain] = matches || [] const [domain] = matches || []
const favicon = domain ? `${domain}/favicon.ico` : null const favicon = domain ? `${domain}/favicon.ico` : null
return { url, favicon } return { id, url, favicon }
}) })
} }
}, },
@ -83,6 +96,7 @@ export default {
mutation: gql` mutation: gql`
mutation($url: String!) { mutation($url: String!) {
CreateSocialMedia(url: $url) { CreateSocialMedia(url: $url) {
id
url url
} }
} }
@ -101,11 +115,51 @@ export default {
}) })
} }
}) })
.then( .then(() => {
this.$toast.success(this.$t('settings.social-media.success')), this.$toast.success(this.$t('settings.social-media.successAdd')),
(this.value = '') (this.value = '')
) })
.catch(error => {
this.$toast.error(error.message)
})
},
handleDeleteSocialMedia(link) {
this.$apollo
.mutate({
mutation: gql`
mutation($id: ID!) {
DeleteSocialMedia(id: $id) {
id
url
}
}
`,
variables: {
id: link.id
},
update: (store, { data }) => {
const socialMedia = this.currentUser.socialMedia.filter(
element => element.id !== link.id
)
this.setCurrentUser({
...this.currentUser,
socialMedia
})
}
})
.then(() => {
this.$toast.success(this.$t('settings.social-media.successDelete'))
})
.catch(error => {
this.$toast.error(error.message)
})
} }
} }
} }
</script> </script>
<style lang="scss">
.layout-leave-active {
opacity: 0.4;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -1503,15 +1503,15 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
codecov@^3.3.0: codecov@^3.4.0:
version "3.3.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/codecov/-/codecov-3.3.0.tgz#7bf337b3f7b0474606b5c31c56dd9e44e395e15d" resolved "https://registry.yarnpkg.com/codecov/-/codecov-3.4.0.tgz#7d16d9d82b0ce20efe5dbf66245a9740779ff61b"
integrity sha512-S70c3Eg9SixumOvxaKE/yKUxb9ihu/uebD9iPO2IR73IdP4i6ZzjXEULj3d0HeyWPr0DqBfDkjNBWxURjVO5hw== integrity sha512-+vtyL1B11MWiRIBaPnsIALKKpLFck9m6QdyI20ZnG8WqLG2cxwCTW9x/LbG4Ht8b81equZWw5xLcr+0BIvmdJQ==
dependencies: dependencies:
argv "^0.0.2" argv "^0.0.2"
ignore-walk "^3.0.1" ignore-walk "^3.0.1"
js-yaml "^3.12.0" js-yaml "^3.13.0"
teeny-request "^3.7.0" teeny-request "^3.11.3"
urlgrey "^0.4.4" urlgrey "^0.4.4"
coffee-react-transform@^3.1.0: coffee-react-transform@^3.1.0:
@ -3055,7 +3055,7 @@ js-levenshtein@^1.1.3:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.12.0: js-yaml@^3.13.0, js-yaml@^3.9.0:
version "3.13.1" version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
@ -3063,14 +3063,6 @@ js-yaml@^3.12.0:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
js-yaml@^3.9.0:
version "3.13.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.0.tgz#38ee7178ac0eea2c97ff6d96fff4b18c7d8cf98e"
integrity sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
jsbn@~0.1.0: jsbn@~0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -4682,7 +4674,7 @@ tar@^4:
safe-buffer "^5.1.2" safe-buffer "^5.1.2"
yallist "^3.0.2" yallist "^3.0.2"
teeny-request@^3.7.0: teeny-request@^3.11.3:
version "3.11.3" version "3.11.3"
resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-3.11.3.tgz#335c629f7645e5d6599362df2f3230c4cbc23a55" resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-3.11.3.tgz#335c629f7645e5d6599362df2f3230c4cbc23a55"
integrity sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw== integrity sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==