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,9 +3,6 @@ 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(
@ -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('create social media', () => {
beforeEach(() => {
variables = { url }
mutation = createSocialMediaMutation
})
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
client = new GraphQLClient(host) client = new GraphQLClient(host)
const variables = { await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
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({ headers = await login(userParams)
email: 'test@example.org', client = new GraphQLClient(host, { headers })
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
}) })
it('creates social media with correct URL', async () => { it('creates social media with correct URL', async () => {
const variables = { await expect(client.request(mutation, variables)).resolves.toEqual(
url: 'http://nsosp.org',
}
await expect(client.request(mutationC, variables)).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
CreateSocialMedia: { CreateSocialMedia: {
id: expect.any(String), id: expect.any(String),
url: 'http://nsosp.org', url: url,
}, },
}), }),
) )
}) })
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 = { variables = { url: '' }
url: '',
} await expect(client.request(mutation, variables)).rejects.toThrow(
await expect(client.request(mutationC, variables)).rejects.toThrow(
'"url" is not allowed to be empty', '"url" is not allowed to be empty',
) )
}) })
it('validates URLs', async () => { it('rejects invalid URLs', async () => {
const variables = { variables = { url: 'not-a-url' }
url: 'not-a-url',
}
await expect(client.request(mutationC, variables)).rejects.toThrow( await expect(client.request(createSocialMediaMutation, variables)).rejects.toThrow(
'"url" must be a valid uri', '"url" must be a valid uri',
) )
}) })
}) })
}) })
describe('update social media', () => {
beforeEach(async () => {
headers = await login(ownerParams)
client = new GraphQLClient(host, { headers })
const { CreateSocialMedia } = await client.request(createSocialMediaMutation, { url })
const { id } = CreateSocialMedia
variables = { url: newUrl, id }
mutation = updateSocialMediaMutation
})
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', () => {
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', () => {
beforeEach(() => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.contains('div')).toBe(true) form = wrapper.find('form')
input = wrapper.find('input#addSocialMedia')
submitButton = wrapper.find('button')
}) })
describe('given currentUser has a social media account linked', () => { it('requires the link to be a valid url', () => {
input.setValue('some value')
form.trigger('submit')
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
})
it('displays an error message when not saved successfully', async () => {
mocks.$apollo.mutate.mockRejectedValue({ message: 'Ouch!' })
input.setValue(newSocialMediaUrl)
form.trigger('submit')
await flushPromises()
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('given existing social media links', () => {
beforeEach(() => { beforeEach(() => {
getters = { getters = {
'auth/user': () => { 'auth/user': () => ({
return {
socialMedia: [{ id: 's1', url: socialMediaUrl }], socialMedia: [{ id: 's1', url: socialMediaUrl }],
}
},
}
})
it("displays a link to the currentUser's social media", () => {
wrapper = Wrapper()
const socialMediaLink = wrapper.find('a').attributes().href
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 }],
}
},
} }
wrapper = Wrapper()
form = wrapper.find('form')
}) })
it('displays a trash sympol after a social media and allows the user to delete it', () => { describe('for each link it', () => {
wrapper = Wrapper() it('displays the favicon', () => {
const deleteSelector = wrapper.find({ name: 'delete' }) expect(wrapper.find(`img[src="${faviconUrl}"]`).exists()).toBe(true)
expect(deleteSelector).toEqual({ selector: 'Component' }) })
const icon = wrapper.find({ name: 'trash' })
icon.trigger('click') it('displays the url', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1) 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)
}) })
}) })
describe('currentUser does not have a social media account linked', () => { it('does not accept a duplicate url', () => {
it('allows a user to add a social media link', () => { input = wrapper.find('input#addSocialMedia')
wrapper = Wrapper()
input = wrapper.find({ name: 'social-media' }) input.setValue(socialMediaUrl)
input.element.value = socialMediaUrl form.trigger('submit')
input.trigger('input')
submitBtn = wrapper.find('.ds-button') expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
submitBtn.trigger('click') })
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).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-form
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
@submit="handleSubmitSocialMedia"
>
<ds-card :header="$t('settings.social-media.name')"> <ds-card :header="$t('settings.social-media.name')">
<ds-space v-if="socialMediaLinks" margin-top="base" margin="x-small"> <ds-space v-if="socialMediaLinks" margin-top="base" margin="x-small">
<ds-list> <ds-list>
<ds-list-item v-for="link in socialMediaLinks" :key="link.id"> <ds-list-item v-for="link in socialMediaLinks" :key="link.id" class="list-item--high">
<ds-input
v-if="editingLink.id === link.id"
id="editSocialMedia"
model="socialMediaUrl"
type="text"
:placeholder="$t('settings.social-media.placeholder')"
/>
<template v-else>
<a :href="link.url" target="_blank"> <a :href="link.url" target="_blank">
<img :src="link.favicon | proxyApiUrl" alt="Social Media link" width="16" height="16" /> <img :src="link.favicon" alt="Link:" height="16" width="16" />
{{ link.url }} {{ link.url }}
</a> </a>
&nbsp;&nbsp; <span class="divider">|</span>
<span class="layout-leave-active">|</span> <a name="edit" @click="handleEditSocialMedia(link)">
&nbsp;&nbsp; <ds-icon
<ds-icon name="edit" class="layout-leave-active" /> :aria-label="$t('actions.edit')"
<a name="delete" @click="handleDeleteSocialMedia(link)"> class="icon-button"
<ds-icon name="trash" /> name="edit"
:title="$t('actions.edit')"
/>
</a> </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-item>
</ds-list> </ds-list>
</ds-space> </ds-space>
<ds-space margin-top="base"> <ds-space margin-top="base">
<div>
<ds-input <ds-input
v-model="value" v-if="!editingLink.id"
id="addSocialMedia"
model="socialMediaUrl"
type="text"
:placeholder="$t('settings.social-media.placeholder')" :placeholder="$t('settings.social-media.placeholder')"
name="social-media"
:schema="{ type: 'url' }"
/> />
</div>
<ds-space margin-top="base"> <ds-space margin-top="base">
<div> <ds-button primary :disabled="disabled">
<ds-button primary @click="handleAddSocialMedia"> {{ editingLink.id ? $t('actions.save') : $t('settings.social-media.submit') }}
{{ $t('settings.social-media.submit') }} </ds-button>
<ds-button v-if="editingLink.id" id="cancel" ghost @click="handleCancel()">
{{ $t('actions.cancel') }}
</ds-button> </ds-button>
</div>
</ds-space> </ds-space>
</ds-space> </ds-space>
</ds-card> </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) { handleEditSocialMedia(link) {
id this.editingLink = link
url this.formData.socialMediaUrl = link.url
},
handleInput(data) {
this.disabled = true
},
handleInputValid(data) {
if (data.socialMediaUrl.length < 1) {
this.disabled = true
} else {
this.disabled = false
} }
}
`,
variables: {
url: this.value,
}, },
update: (store, { data }) => { async handleDeleteSocialMedia(link) {
const socialMedia = [...this.currentUser.socialMedia, data.CreateSocialMedia] try {
this.setCurrentUser({ await this.$apollo.mutate({
...this.currentUser,
socialMedia,
})
},
})
.then(() => {
this.$toast.success(this.$t('settings.social-media.successAdd'))
this.value = ''
})
.catch(error => {
this.$toast.error(error.message)
})
},
handleDeleteSocialMedia(link) {
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) {
this.$toast.error(err.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'),
}) })
.catch(error => { },
this.$toast.error(error.message)
}) })
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"