Merge pull request #1053 from Human-Connection/refine-social-media

🍰 Refine social media
This commit is contained in:
Wolfgang Huß 2019-07-29 16:08:08 +02:00 committed by GitHub
commit a542b33f79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 535 additions and 225 deletions

View File

@ -163,6 +163,7 @@ const permissions = shield(
DeletePost: isAuthor, DeletePost: isAuthor,
report: isAuthenticated, report: isAuthenticated,
CreateSocialMedia: isAuthenticated, CreateSocialMedia: isAuthenticated,
UpdateSocialMedia: isAuthenticated,
DeleteSocialMedia: isAuthenticated, DeleteSocialMedia: isAuthenticated,
// AddBadgeRewarded: isAdmin, // AddBadgeRewarded: isAdmin,
// RemoveBadgeRewarded: isAdmin, // RemoveBadgeRewarded: isAdmin,

View File

@ -3,14 +3,11 @@ 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(
`MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId}) `MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId})
MERGE (socialMedia)<-[:OWNED]-(owner) MERGE (socialMedia)<-[:OWNED]-(owner)
RETURN owner`, RETURN owner`,
{ {
userId: context.user.id, userId: context.user.id,
@ -26,5 +23,21 @@ export default {
return socialMedia return socialMedia
}, },
UpdateSocialMedia: async (object, params, context, resolveInfo) => {
const session = context.driver.session()
await session.run(
`MATCH (owner: User { id: $userId })-[:OWNED]->(socialMedia: SocialMedia { id: $socialMediaId })
SET socialMedia.url = $socialMediaUrl
RETURN owner`,
{
userId: context.user.id,
socialMediaId: params.id,
socialMediaUrl: params.url,
},
)
session.close()
return params
},
}, },
} }

View File

@ -5,9 +5,26 @@ import { host, login, gql } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
describe('SocialMedia', () => { describe('SocialMedia', () => {
let client let client, headers, variables, mutation
let headers
const mutationC = gql` const ownerParams = {
email: 'owner@example.com',
password: '1234',
id: '1234',
name: 'Pippi Langstrumpf',
}
const userParams = {
email: 'someuser@example.com',
password: 'abcd',
id: 'abcd',
name: 'Kalle Blomqvist',
}
const url = 'https://twitter.com/pippi-langstrumpf'
const newUrl = 'https://twitter.com/bullerby'
const createSocialMediaMutation = gql`
mutation($url: String!) { mutation($url: String!) {
CreateSocialMedia(url: $url) { CreateSocialMedia(url: $url) {
id id
@ -15,7 +32,15 @@ describe('SocialMedia', () => {
} }
} }
` `
const mutationD = gql` const updateSocialMediaMutation = gql`
mutation($id: ID!, $url: String!) {
UpdateSocialMedia(id: $id, url: $url) {
id
url
}
}
`
const deleteSocialMediaMutation = gql`
mutation($id: ID!) { mutation($id: ID!) {
DeleteSocialMedia(id: $id) { DeleteSocialMedia(id: $id) {
id id
@ -24,92 +49,139 @@ describe('SocialMedia', () => {
} }
` `
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { await factory.create('User', userParams)
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg', await factory.create('User', ownerParams)
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
name: 'Matilde Hermiston',
slug: 'matilde-hermiston',
role: 'user',
email: 'test@example.org',
password: '1234',
})
}) })
afterEach(async () => { afterEach(async () => {
await factory.cleanDatabase() await factory.cleanDatabase()
}) })
describe('unauthenticated', () => { describe('create social media', () => {
it('throws authorization error', async () => { beforeEach(() => {
client = new GraphQLClient(host) variables = { url }
const variables = { mutation = createSocialMediaMutation
url: 'http://nsosp.org', })
}
await expect(client.request(mutationC, variables)).rejects.toThrow('Not Authorised') describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated', () => {
beforeEach(async () => {
headers = await login(userParams)
client = new GraphQLClient(host, { headers })
})
it('creates social media with correct URL', async () => {
await expect(client.request(mutation, variables)).resolves.toEqual(
expect.objectContaining({
CreateSocialMedia: {
id: expect.any(String),
url: url,
},
}),
)
})
it('rejects empty string', async () => {
variables = { url: '' }
await expect(client.request(mutation, variables)).rejects.toThrow(
'"url" is not allowed to be empty',
)
})
it('rejects invalid URLs', async () => {
variables = { url: 'not-a-url' }
await expect(client.request(createSocialMediaMutation, variables)).rejects.toThrow(
'"url" must be a valid uri',
)
})
}) })
}) })
describe('authenticated', () => { describe('update social media', () => {
beforeEach(async () => { beforeEach(async () => {
headers = await login({ headers = await login(ownerParams)
email: 'test@example.org', client = new GraphQLClient(host, { headers })
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
})
it('creates social media with correct URL', async () => { const { CreateSocialMedia } = await client.request(createSocialMediaMutation, { url })
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 { id } = CreateSocialMedia
const deletionVariables = { variables = { url: newUrl, id }
id, mutation = updateSocialMediaMutation
}
const expected = {
DeleteSocialMedia: {
id: id,
url: 'http://nsosp.org',
},
}
await expect(client.request(mutationD, deletionVariables)).resolves.toEqual(expected)
}) })
it('rejects empty string', async () => { describe('unauthenticated', () => {
const variables = { it('throws authorization error', async () => {
url: '', client = new GraphQLClient(host)
} await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
await expect(client.request(mutationC, variables)).rejects.toThrow( })
'"url" is not allowed to be empty',
)
}) })
it('validates URLs', async () => { describe('authenticated as other user', () => {
const variables = { // TODO: make sure it throws an authorization error
url: 'not-a-url', })
}
await expect(client.request(mutationC, variables)).rejects.toThrow( describe('authenticated as owner', () => {
'"url" must be a valid uri', it('updates social media', async () => {
) const expected = { UpdateSocialMedia: { ...variables } }
await expect(client.request(mutation, variables)).resolves.toEqual(
expect.objectContaining(expected),
)
})
describe('given a non-existent id', () => {
// TODO: make sure it throws an error
})
})
})
describe('delete social media', () => {
beforeEach(async () => {
headers = await login(ownerParams)
client = new GraphQLClient(host, { headers })
const { CreateSocialMedia } = await client.request(createSocialMediaMutation, { url })
const { id } = CreateSocialMedia
variables = { id }
mutation = deleteSocialMediaMutation
})
describe('unauthenticated', () => {
it('throws authorization error', async () => {
client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
})
})
describe('authenticated as other user', () => {
// TODO: make sure it throws an authorization error
})
describe('authenticated as owner', () => {
beforeEach(async () => {
headers = await login(ownerParams)
client = new GraphQLClient(host, { headers })
})
it('deletes social media', async () => {
const expected = {
DeleteSocialMedia: {
id: variables.id,
url: url,
},
}
await expect(client.request(mutation, variables)).resolves.toEqual(expected)
})
}) })
}) })
}) })

View File

@ -79,7 +79,7 @@ Then('I should be on the {string} page', page => {
}) })
When('I add a social media link', () => { When('I add a social media link', () => {
cy.get("input[name='social-media']") cy.get('input#addSocialMedia')
.type('https://freeradical.zone/peter-pan') .type('https://freeradical.zone/peter-pan')
.get('button') .get('button')
.contains('Add link') .contains('Add link')
@ -98,7 +98,7 @@ Then('the new social media link shows up on the page', () => {
Given('I have added a social media link', () => { Given('I have added a social media link', () => {
cy.openPage('/settings/my-social-media') cy.openPage('/settings/my-social-media')
.get("input[name='social-media']") .get('input#addSocialMedia')
.type('https://freeradical.zone/peter-pan') .type('https://freeradical.zone/peter-pan')
.get('button') .get('button')
.contains('Add link') .contains('Add link')
@ -121,3 +121,34 @@ Then('it gets deleted successfully', () => {
cy.get('.iziToast-message') cy.get('.iziToast-message')
.should('contain', 'Deleted social media') .should('contain', 'Deleted social media')
}) })
When('I start editing a social media link', () => {
cy.get("a[name='edit']")
.click()
})
Then('I can cancel editing', () => {
cy.get('button#cancel')
.click()
.get('input#editSocialMedia')
.should('have.length', 0)
})
When('I edit and save the link', () => {
cy.get('input#editSocialMedia')
.clear()
.type('https://freeradical.zone/tinkerbell')
.get('button')
.contains('Save')
.click()
})
Then('the new url is displayed', () => {
cy.get("a[href='https://freeradical.zone/tinkerbell']")
.should('have.length', 1)
})
Then('the old url is not displayed', () => {
cy.get("a[href='https://freeradical.zone/peter-pan']")
.should('have.length', 0)
})

View File

@ -15,7 +15,7 @@ Feature: List Social Media Accounts
Then it gets saved successfully Then it gets saved successfully
And the new social media link shows up on the page And the new social media link shows up on the page
Scenario: Other user's viewing my Social Media Scenario: Other users viewing my Social Media
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
@ -27,3 +27,16 @@ Feature: List Social Media Accounts
Given I have added a social media link Given I have added a social media link
When I delete a social media link When I delete a social media link
Then it gets deleted successfully Then it gets deleted successfully
Scenario: Editing 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 start editing a social media link
Then I can cancel editing
When I start editing a social media link
And I edit and save the link
Then it gets saved successfully
And the new url is displayed
But the old url is not displayed

View File

@ -168,7 +168,8 @@
}, },
"social-media": { "social-media": {
"name": "Soziale Medien", "name": "Soziale Medien",
"placeholder": "Füge eine Social-Media URL hinzu", "placeholder": "Deine Social-Media URL",
"requireUnique": "Dieser Link existiert bereits",
"submit": "Link hinzufügen", "submit": "Link hinzufügen",
"successAdd": "Social-Media hinzugefügt. Profil aktualisiert!", "successAdd": "Social-Media hinzugefügt. Profil aktualisiert!",
"successDelete": "Social-Media gelöscht. Profil aktualisiert!" "successDelete": "Social-Media gelöscht. Profil aktualisiert!"
@ -286,6 +287,7 @@
"reportContent": "Melden", "reportContent": "Melden",
"validations": { "validations": {
"email": "muss eine gültige E-Mail Adresse sein", "email": "muss eine gültige E-Mail Adresse sein",
"url": "muss eine gültige URL sein",
"verification-code": "muss genau 6 Buchstaben lang sein" "verification-code": "muss genau 6 Buchstaben lang sein"
} }
}, },

View File

@ -169,7 +169,8 @@
}, },
"social-media": { "social-media": {
"name": "Social media", "name": "Social media",
"placeholder": "Add social media url", "placeholder": "Your social media url",
"requireUnique": "You added this url already",
"submit": "Add link", "submit": "Add link",
"successAdd": "Added social media. Updated user profile!", "successAdd": "Added social media. Updated user profile!",
"successDelete": "Deleted social media. Updated user profile!" "successDelete": "Deleted social media. Updated user profile!"
@ -288,6 +289,7 @@
"reportContent": "Report", "reportContent": "Report",
"validations": { "validations": {
"email": "must be a valid email address", "email": "must be a valid email address",
"url": "must be a valid URL",
"verification-code": "must be 6 characters long" "verification-code": "must be 6 characters long"
} }
}, },

View File

@ -101,6 +101,7 @@
"eslint-plugin-promise": "~4.2.1", "eslint-plugin-promise": "~4.2.1",
"eslint-plugin-standard": "~4.0.0", "eslint-plugin-standard": "~4.0.0",
"eslint-plugin-vue": "~5.2.3", "eslint-plugin-vue": "~5.2.3",
"flush-promises": "^1.0.2",
"fuse.js": "^3.4.5", "fuse.js": "^3.4.5",
"jest": "~24.8.0", "jest": "~24.8.0",
"node-sass": "~4.12.0", "node-sass": "~4.12.0",

View File

@ -1,4 +1,5 @@
import { mount, createLocalVue } from '@vue/test-utils' import { mount, createLocalVue } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import MySocialMedia from './my-social-media.vue' import MySocialMedia from './my-social-media.vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import Styleguide from '@human-connection/styleguide' import Styleguide from '@human-connection/styleguide'
@ -12,23 +13,17 @@ localVue.use(Filters)
describe('my-social-media.vue', () => { describe('my-social-media.vue', () => {
let wrapper let wrapper
let store
let mocks let mocks
let getters let getters
let input
let submitBtn
const socialMediaUrl = 'https://freeradical.zone/@mattwr18' const socialMediaUrl = 'https://freeradical.zone/@mattwr18'
const newSocialMediaUrl = 'https://twitter.com/mattwr18'
const faviconUrl = 'https://freeradical.zone/favicon.ico'
beforeEach(() => { beforeEach(() => {
mocks = { mocks = {
$t: jest.fn(), $t: jest.fn(),
$apollo: { $apollo: {
mutate: jest mutate: jest.fn(),
.fn()
.mockRejectedValue({ message: 'Ouch!' })
.mockResolvedValueOnce({
data: { CreateSocialMeda: { id: 's1', url: socialMediaUrl } },
}),
}, },
$toast: { $toast: {
error: jest.fn(), error: jest.fn(),
@ -43,79 +38,161 @@ describe('my-social-media.vue', () => {
}) })
describe('mount', () => { describe('mount', () => {
let form, input, submitButton
const Wrapper = () => { const Wrapper = () => {
store = new Vuex.Store({ const store = new Vuex.Store({
getters, getters,
}) })
return mount(MySocialMedia, { store, mocks, localVue }) return mount(MySocialMedia, { store, mocks, localVue })
} }
it('renders', () => { describe('adding social media link', () => {
wrapper = Wrapper()
expect(wrapper.contains('div')).toBe(true)
})
describe('given currentUser has a social media account linked', () => {
beforeEach(() => { beforeEach(() => {
getters = {
'auth/user': () => {
return {
socialMedia: [{ id: 's1', url: socialMediaUrl }],
}
},
}
})
it("displays a link to the currentUser's social media", () => {
wrapper = Wrapper() wrapper = Wrapper()
const socialMediaLink = wrapper.find('a').attributes().href form = wrapper.find('form')
expect(socialMediaLink).toBe(socialMediaUrl) input = wrapper.find('input#addSocialMedia')
submitButton = wrapper.find('button')
}) })
beforeEach(() => { it('requires the link to be a valid url', () => {
mocks = { input.setValue('some value')
$t: jest.fn(), form.trigger('submit')
$apollo: {
mutate: jest expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
.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', () => { it('displays an error message when not saved successfully', async () => {
wrapper = Wrapper() mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' })
const deleteSelector = wrapper.find({ name: 'delete' }) input.setValue(newSocialMediaUrl)
expect(deleteSelector).toEqual({ selector: 'Component' }) form.trigger('submit')
const icon = wrapper.find({ name: 'trash' })
icon.trigger('click') await flushPromises()
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
expect(mocks.$toast.error).toHaveBeenCalledTimes(1)
})
describe('success', () => {
beforeEach(() => {
mocks.$apollo.mutate.mockResolvedValue({
data: { CreateSocialMedia: { id: 's2', url: newSocialMediaUrl } },
})
input.setValue(newSocialMediaUrl)
form.trigger('submit')
})
it('sends the new url to the backend', () => {
const expected = expect.objectContaining({
variables: { url: newSocialMediaUrl },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('displays a success message', async () => {
await flushPromises()
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
it('clears the form', async () => {
await flushPromises()
expect(input.value).toBe(undefined)
expect(submitButton.vm.$attrs.disabled).toBe(true)
})
}) })
}) })
describe('currentUser does not have a social media account linked', () => { describe('given existing social media links', () => {
it('allows a user to add a social media link', () => { beforeEach(() => {
getters = {
'auth/user': () => ({
socialMedia: [{ id: 's1', url: socialMediaUrl }],
}),
}
wrapper = Wrapper() wrapper = Wrapper()
input = wrapper.find({ name: 'social-media' }) form = wrapper.find('form')
input.element.value = socialMediaUrl })
input.trigger('input')
submitBtn = wrapper.find('.ds-button') describe('for each link it', () => {
submitBtn.trigger('click') it('displays the favicon', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) expect(wrapper.find(`img[src="${faviconUrl}"]`).exists()).toBe(true)
})
it('displays the url', () => {
expect(wrapper.find(`a[href="${socialMediaUrl}"]`).exists()).toBe(true)
})
it('displays the edit button', () => {
expect(wrapper.find('a[name="edit"]').exists()).toBe(true)
})
it('displays the delete button', () => {
expect(wrapper.find('a[name="delete"]').exists()).toBe(true)
})
})
it('does not accept a duplicate url', () => {
input = wrapper.find('input#addSocialMedia')
input.setValue(socialMediaUrl)
form.trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
describe('editing social media link', () => {
beforeEach(() => {
const editButton = wrapper.find('a[name="edit"]')
editButton.trigger('click')
input = wrapper.find('input#editSocialMedia')
})
it('disables adding new links while editing', () => {
const addInput = wrapper.find('input#addSocialMedia')
expect(addInput.exists()).toBe(false)
})
it('sends the new url to the backend', () => {
const expected = expect.objectContaining({
variables: { id: 's1', url: newSocialMediaUrl },
})
input.setValue(newSocialMediaUrl)
form.trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('allows the user to cancel editing', () => {
const cancelButton = wrapper.find('button#cancel')
cancelButton.trigger('click')
expect(wrapper.find('input#editSocialMedia').exists()).toBe(false)
})
})
describe('deleting social media link', () => {
beforeEach(() => {
const deleteButton = wrapper.find('a[name="delete"]')
deleteButton.trigger('click')
})
it('sends the link id to the backend', () => {
const expected = expect.objectContaining({
variables: { id: 's1' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('displays a success message', async () => {
await flushPromises()
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
}) })
}) })
}) })

View File

@ -1,49 +1,90 @@
<template> <template>
<ds-card :header="$t('settings.social-media.name')"> <ds-form
<ds-space v-if="socialMediaLinks" margin-top="base" margin="x-small"> v-model="formData"
<ds-list> :schema="formSchema"
<ds-list-item v-for="link in socialMediaLinks" :key="link.id"> @input="handleInput"
<a :href="link.url" target="_blank"> @input-valid="handleInputValid"
<img :src="link.favicon | proxyApiUrl" alt="Social Media link" width="16" height="16" /> @submit="handleSubmitSocialMedia"
{{ link.url }} >
</a> <ds-card :header="$t('settings.social-media.name')">
&nbsp;&nbsp; <ds-space v-if="socialMediaLinks" margin-top="base" margin="x-small">
<span class="layout-leave-active">|</span> <ds-list>
&nbsp;&nbsp; <ds-list-item v-for="link in socialMediaLinks" :key="link.id" class="list-item--high">
<ds-icon name="edit" class="layout-leave-active" /> <ds-input
<a name="delete" @click="handleDeleteSocialMedia(link)"> v-if="editingLink.id === link.id"
<ds-icon name="trash" /> id="editSocialMedia"
</a> model="socialMediaUrl"
</ds-list-item> type="text"
</ds-list> :placeholder="$t('settings.social-media.placeholder')"
</ds-space> />
<ds-space margin-top="base">
<div> <template v-else>
<ds-input <a :href="link.url" target="_blank">
v-model="value" <img :src="link.favicon" alt="Link:" height="16" width="16" />
:placeholder="$t('settings.social-media.placeholder')" {{ link.url }}
name="social-media" </a>
:schema="{ type: 'url' }" <span class="divider">|</span>
/> <a name="edit" @click="handleEditSocialMedia(link)">
</div> <ds-icon
<ds-space margin-top="base"> :aria-label="$t('actions.edit')"
<div> class="icon-button"
<ds-button primary @click="handleAddSocialMedia"> name="edit"
{{ $t('settings.social-media.submit') }} :title="$t('actions.edit')"
</ds-button> />
</div> </a>
<a name="delete" @click="handleDeleteSocialMedia(link)">
<ds-icon
:aria-label="$t('actions.delete')"
class="icon-button"
name="trash"
:title="$t('actions.delete')"
/>
</a>
</template>
</ds-list-item>
</ds-list>
</ds-space> </ds-space>
</ds-space>
</ds-card> <ds-space margin-top="base">
<ds-input
v-if="!editingLink.id"
id="addSocialMedia"
model="socialMediaUrl"
type="text"
:placeholder="$t('settings.social-media.placeholder')"
/>
<ds-space margin-top="base">
<ds-button primary :disabled="disabled">
{{ editingLink.id ? $t('actions.save') : $t('settings.social-media.submit') }}
</ds-button>
<ds-button v-if="editingLink.id" id="cancel" ghost @click="handleCancel()">
{{ $t('actions.cancel') }}
</ds-button>
</ds-space>
</ds-space>
</ds-card>
</ds-form>
</template> </template>
<script> <script>
import unionBy from 'lodash/unionBy'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { mapGetters, mapMutations } from 'vuex' import { mapGetters, mapMutations } from 'vuex'
export default { export default {
data() { data() {
return { return {
value: '', formData: {
socialMediaUrl: '',
},
formSchema: {
socialMediaUrl: {
type: 'url',
message: this.$t('common.validations.url'),
},
},
disabled: true,
editingLink: {},
} }
}, },
computed: { computed: {
@ -51,11 +92,10 @@ export default {
currentUser: 'auth/user', currentUser: 'auth/user',
}), }),
socialMediaLinks() { socialMediaLinks() {
const domainRegex = /^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g
const { socialMedia = [] } = this.currentUser const { socialMedia = [] } = this.currentUser
return socialMedia.map(socialMedia => { return socialMedia.map(({ id, url }) => {
const { id, url } = socialMedia const [domain] = url.match(domainRegex) || []
const matches = url.match(/^(?:https?:\/\/)?(?:[^@\n])?(?:www\.)?([^:/\n?]+)/g)
const [domain] = matches || []
const favicon = domain ? `${domain}/favicon.ico` : null const favicon = domain ? `${domain}/favicon.ico` : null
return { id, url, favicon } return { id, url, favicon }
}) })
@ -65,39 +105,28 @@ export default {
...mapMutations({ ...mapMutations({
setCurrentUser: 'auth/SET_USER', setCurrentUser: 'auth/SET_USER',
}), }),
handleAddSocialMedia() { handleCancel() {
this.$apollo this.editingLink = {}
.mutate({ this.formData.socialMediaUrl = ''
mutation: gql` this.disabled = true
mutation($url: String!) {
CreateSocialMedia(url: $url) {
id
url
}
}
`,
variables: {
url: this.value,
},
update: (store, { data }) => {
const socialMedia = [...this.currentUser.socialMedia, data.CreateSocialMedia]
this.setCurrentUser({
...this.currentUser,
socialMedia,
})
},
})
.then(() => {
this.$toast.success(this.$t('settings.social-media.successAdd'))
this.value = ''
})
.catch(error => {
this.$toast.error(error.message)
})
}, },
handleDeleteSocialMedia(link) { handleEditSocialMedia(link) {
this.$apollo this.editingLink = link
.mutate({ this.formData.socialMediaUrl = link.url
},
handleInput(data) {
this.disabled = true
},
handleInputValid(data) {
if (data.socialMediaUrl.length < 1) {
this.disabled = true
} else {
this.disabled = false
}
},
async handleDeleteSocialMedia(link) {
try {
await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation($id: ID!) { mutation($id: ID!) {
DeleteSocialMedia(id: $id) { DeleteSocialMedia(id: $id) {
@ -119,19 +148,83 @@ export default {
}) })
}, },
}) })
.then(() => {
this.$toast.success(this.$t('settings.social-media.successDelete')) this.$toast.success(this.$t('settings.social-media.successDelete'))
}) } catch (err) {
.catch(error => { this.$toast.error(err.message)
this.$toast.error(error.message) }
},
async handleSubmitSocialMedia() {
const isEditing = !!this.editingLink.id
const url = this.formData.socialMediaUrl
const duplicateUrl = this.socialMediaLinks.find(link => link.url === url)
if (duplicateUrl && duplicateUrl.id !== this.editingLink.id) {
return this.$toast.error(this.$t('settings.social-media.requireUnique'))
}
let mutation = gql`
mutation($url: String!) {
CreateSocialMedia(url: $url) {
id
url
}
}
`
const variables = { url }
let successMessage = this.$t('settings.social-media.successAdd')
if (isEditing) {
mutation = gql`
mutation($id: ID!, $url: String!) {
UpdateSocialMedia(id: $id, url: $url) {
id
url
}
}
`
variables.id = this.editingLink.id
successMessage = this.$t('settings.data.success')
}
try {
await this.$apollo.mutate({
mutation,
variables,
update: (store, { data }) => {
const newSocialMedia = isEditing ? data.UpdateSocialMedia : data.CreateSocialMedia
this.setCurrentUser({
...this.currentUser,
socialMedia: unionBy([newSocialMedia], this.currentUser.socialMedia, 'id'),
})
},
}) })
this.$toast.success(successMessage)
this.formData.socialMediaUrl = ''
this.disabled = true
this.editingLink = {}
} catch (err) {
this.$toast.error(err.message)
}
}, },
}, },
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.layout-leave-active { .divider {
opacity: 0.4; opacity: 0.4;
padding: 0 $space-small;
}
.icon-button {
cursor: pointer;
}
.list-item--high {
.ds-list-item-prefix {
align-self: center;
}
} }
</style> </style>

View File

@ -4911,6 +4911,11 @@ flatten@^1.0.2:
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I= integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=
flush-promises@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flush-promises/-/flush-promises-1.0.2.tgz#4948fd58f15281fed79cbafc86293d5bb09b2ced"
integrity sha512-G0sYfLQERwKz4+4iOZYQEZVpOt9zQrlItIxQAAYAWpfby3gbHrx0osCHz5RLl/XoXevXk0xoN4hDFky/VV9TrA==
flush-write-stream@^1.0.0: flush-write-stream@^1.0.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"