mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into 384-emotions-on-posts
This commit is contained in:
commit
08899a4af9
@ -48,7 +48,7 @@
|
|||||||
"apollo-client": "~2.6.3",
|
"apollo-client": "~2.6.3",
|
||||||
"apollo-link-context": "~1.0.18",
|
"apollo-link-context": "~1.0.18",
|
||||||
"apollo-link-http": "~1.5.15",
|
"apollo-link-http": "~1.5.15",
|
||||||
"apollo-server": "~2.7.2",
|
"apollo-server": "~2.8.0",
|
||||||
"apollo-server-express": "^2.7.2",
|
"apollo-server-express": "^2.7.2",
|
||||||
"bcryptjs": "~2.4.3",
|
"bcryptjs": "~2.4.3",
|
||||||
"cheerio": "~1.0.0-rc.3",
|
"cheerio": "~1.0.0-rc.3",
|
||||||
@ -62,7 +62,7 @@
|
|||||||
"graphql": "~14.4.2",
|
"graphql": "~14.4.2",
|
||||||
"graphql-custom-directives": "~0.2.14",
|
"graphql-custom-directives": "~0.2.14",
|
||||||
"graphql-iso-date": "~3.6.1",
|
"graphql-iso-date": "~3.6.1",
|
||||||
"graphql-middleware": "~3.0.2",
|
"graphql-middleware": "~3.0.3",
|
||||||
"graphql-shield": "~6.0.4",
|
"graphql-shield": "~6.0.4",
|
||||||
"graphql-tag": "~2.10.1",
|
"graphql-tag": "~2.10.1",
|
||||||
"helmet": "~3.20.0",
|
"helmet": "~3.20.0",
|
||||||
@ -106,7 +106,7 @@
|
|||||||
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
|
||||||
"@babel/preset-env": "~7.5.5",
|
"@babel/preset-env": "~7.5.5",
|
||||||
"@babel/register": "~7.5.5",
|
"@babel/register": "~7.5.5",
|
||||||
"apollo-server-testing": "~2.7.2",
|
"apollo-server-testing": "~2.8.0",
|
||||||
"babel-core": "~7.0.0-0",
|
"babel-core": "~7.0.0-0",
|
||||||
"babel-eslint": "~10.0.2",
|
"babel-eslint": "~10.0.2",
|
||||||
"babel-jest": "~24.8.0",
|
"babel-jest": "~24.8.0",
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import { rule, shield, deny, allow, and, or, not } from 'graphql-shield'
|
import { rule, shield, deny, allow, and, or, not } from 'graphql-shield'
|
||||||
|
import { neode } from '../bootstrap/neo4j'
|
||||||
|
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TODO: implement
|
* TODO: implement
|
||||||
@ -7,7 +10,7 @@ import { rule, shield, deny, allow, and, or, not } from 'graphql-shield'
|
|||||||
const isAuthenticated = rule({
|
const isAuthenticated = rule({
|
||||||
cache: 'contextual',
|
cache: 'contextual',
|
||||||
})(async (_parent, _args, ctx, _info) => {
|
})(async (_parent, _args, ctx, _info) => {
|
||||||
return ctx.user !== null
|
return ctx.user != null
|
||||||
})
|
})
|
||||||
|
|
||||||
const isModerator = rule()(async (parent, args, { user }, info) => {
|
const isModerator = rule()(async (parent, args, { user }, info) => {
|
||||||
@ -36,6 +39,14 @@ const isMyOwn = rule({
|
|||||||
return context.user.id === parent.id
|
return context.user.id === parent.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isMySocialMedia = rule({
|
||||||
|
cache: 'no_cache',
|
||||||
|
})(async (_, args, { user }) => {
|
||||||
|
let socialMedia = await instance.find('SocialMedia', args.id)
|
||||||
|
socialMedia = await socialMedia.toJson()
|
||||||
|
return socialMedia.ownedBy.node.id === user.id
|
||||||
|
})
|
||||||
|
|
||||||
const belongsToMe = rule({
|
const belongsToMe = rule({
|
||||||
cache: 'no_cache',
|
cache: 'no_cache',
|
||||||
})(async (_, args, context) => {
|
})(async (_, args, context) => {
|
||||||
@ -170,7 +181,8 @@ const permissions = shield(
|
|||||||
DeletePost: isAuthor,
|
DeletePost: isAuthor,
|
||||||
report: isAuthenticated,
|
report: isAuthenticated,
|
||||||
CreateSocialMedia: isAuthenticated,
|
CreateSocialMedia: isAuthenticated,
|
||||||
DeleteSocialMedia: isAuthenticated,
|
UpdateSocialMedia: isMySocialMedia,
|
||||||
|
DeleteSocialMedia: isMySocialMedia,
|
||||||
// AddBadgeRewarded: isAdmin,
|
// AddBadgeRewarded: isAdmin,
|
||||||
// RemoveBadgeRewarded: isAdmin,
|
// RemoveBadgeRewarded: isAdmin,
|
||||||
reward: isAdmin,
|
reward: isAdmin,
|
||||||
|
|||||||
@ -1,23 +1,8 @@
|
|||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
import Joi from '@hapi/joi'
|
|
||||||
|
|
||||||
const COMMENT_MIN_LENGTH = 1
|
const COMMENT_MIN_LENGTH = 1
|
||||||
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
const NO_POST_ERR_MESSAGE = 'Comment cannot be created without a post!'
|
||||||
|
|
||||||
const validate = schema => {
|
|
||||||
return async (resolve, root, args, context, info) => {
|
|
||||||
const validation = schema.validate(args)
|
|
||||||
if (validation.error) throw new UserInputError(validation.error)
|
|
||||||
return resolve(root, args, context, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const socialMediaSchema = Joi.object().keys({
|
|
||||||
url: Joi.string()
|
|
||||||
.uri()
|
|
||||||
.required(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const validateCommentCreation = async (resolve, root, args, context, info) => {
|
const validateCommentCreation = async (resolve, root, args, context, info) => {
|
||||||
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
const content = args.content.replace(/<(?:.|\n)*?>/gm, '').trim()
|
||||||
const { postId } = args
|
const { postId } = args
|
||||||
@ -57,7 +42,6 @@ const validateUpdateComment = async (resolve, root, args, context, info) => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateSocialMedia: validate(socialMediaSchema),
|
|
||||||
CreateComment: validateCommentCreation,
|
CreateComment: validateCommentCreation,
|
||||||
UpdateComment: validateUpdateComment,
|
UpdateComment: validateUpdateComment,
|
||||||
},
|
},
|
||||||
|
|||||||
15
backend/src/models/SocialMedia.js
Normal file
15
backend/src/models/SocialMedia.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import uuid from 'uuid/v4'
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: { type: 'string', primary: true, default: uuid },
|
||||||
|
url: { type: 'string', uri: true, required: true },
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
ownedBy: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'OWNED_BY',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'in',
|
||||||
|
eager: true,
|
||||||
|
cascade: 'detach',
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -5,4 +5,5 @@ export default {
|
|||||||
User: require('./User.js'),
|
User: require('./User.js'),
|
||||||
InvitationCode: require('./InvitationCode.js'),
|
InvitationCode: require('./InvitationCode.js'),
|
||||||
EmailAddress: require('./EmailAddress.js'),
|
EmailAddress: require('./EmailAddress.js'),
|
||||||
|
SocialMedia: require('./SocialMedia.js'),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export default applyScalars(
|
|||||||
'Notfication',
|
'Notfication',
|
||||||
'Statistics',
|
'Statistics',
|
||||||
'LoggedInUser',
|
'LoggedInUser',
|
||||||
|
'SocialMedia',
|
||||||
],
|
],
|
||||||
// add 'User' here as soon as possible
|
// add 'User' here as soon as possible
|
||||||
},
|
},
|
||||||
@ -30,6 +31,7 @@ export default applyScalars(
|
|||||||
'Notfication',
|
'Notfication',
|
||||||
'Statistics',
|
'Statistics',
|
||||||
'LoggedInUser',
|
'LoggedInUser',
|
||||||
|
'SocialMedia',
|
||||||
],
|
],
|
||||||
// add 'User' here as soon as possible
|
// add 'User' here as soon as possible
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import scrape from './embeds/scraper.js'
|
import scrape from './embeds/scraper.js'
|
||||||
import { undefinedToNull } from '../helpers'
|
import { undefinedToNullResolver } from './helpers/Resolver'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
@ -8,7 +8,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Embed: {
|
Embed: {
|
||||||
...undefinedToNull([
|
...undefinedToNullResolver([
|
||||||
'type',
|
'type',
|
||||||
'title',
|
'title',
|
||||||
'author',
|
'author',
|
||||||
|
|||||||
75
backend/src/schema/resolvers/helpers/Resolver.js
Normal file
75
backend/src/schema/resolvers/helpers/Resolver.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { neode } from '../../../bootstrap/neo4j'
|
||||||
|
|
||||||
|
export const undefinedToNullResolver = list => {
|
||||||
|
const resolvers = {}
|
||||||
|
list.forEach(key => {
|
||||||
|
resolvers[key] = async (parent, params, context, resolveInfo) => {
|
||||||
|
return typeof parent[key] === 'undefined' ? null : parent[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return resolvers
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Resolver(type, options = {}) {
|
||||||
|
const instance = neode()
|
||||||
|
const {
|
||||||
|
idAttribute = 'id',
|
||||||
|
undefinedToNull = [],
|
||||||
|
count = {},
|
||||||
|
hasOne = {},
|
||||||
|
hasMany = {},
|
||||||
|
} = options
|
||||||
|
const _hasResolver = (resolvers, { key, connection }, { returnType }) => {
|
||||||
|
return async (parent, params, context, resolveInfo) => {
|
||||||
|
if (typeof parent[key] !== 'undefined') return parent[key]
|
||||||
|
const id = parent[idAttribute]
|
||||||
|
const statement = `MATCH(:${type} {${idAttribute}: {id}})${connection} RETURN related`
|
||||||
|
const result = await instance.cypher(statement, { id })
|
||||||
|
let response = result.records.map(r => r.get('related').properties)
|
||||||
|
if (returnType === 'object') response = response[0] || null
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const countResolver = obj => {
|
||||||
|
const resolvers = {}
|
||||||
|
for (const [key, connection] of Object.entries(obj)) {
|
||||||
|
resolvers[key] = async (parent, params, context, resolveInfo) => {
|
||||||
|
if (typeof parent[key] !== 'undefined') return parent[key]
|
||||||
|
const id = parent[idAttribute]
|
||||||
|
const statement = `
|
||||||
|
MATCH(u:${type} {${idAttribute}: {id}})${connection}
|
||||||
|
WHERE NOT related.deleted = true AND NOT related.disabled = true
|
||||||
|
RETURN COUNT(DISTINCT(related)) as count
|
||||||
|
`
|
||||||
|
const result = await instance.cypher(statement, { id })
|
||||||
|
const [response] = result.records.map(r => r.get('count').toNumber())
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvers
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasManyResolver = obj => {
|
||||||
|
const resolvers = {}
|
||||||
|
for (const [key, connection] of Object.entries(obj)) {
|
||||||
|
resolvers[key] = _hasResolver(resolvers, { key, connection }, { returnType: 'iterable' })
|
||||||
|
}
|
||||||
|
return resolvers
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasOneResolver = obj => {
|
||||||
|
const resolvers = {}
|
||||||
|
for (const [key, connection] of Object.entries(obj)) {
|
||||||
|
resolvers[key] = _hasResolver(resolvers, { key, connection }, { returnType: 'object' })
|
||||||
|
}
|
||||||
|
return resolvers
|
||||||
|
}
|
||||||
|
const result = {
|
||||||
|
...undefinedToNullResolver(undefinedToNull),
|
||||||
|
...countResolver(count),
|
||||||
|
...hasOneResolver(hasOne),
|
||||||
|
...hasManyResolver(hasMany),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -1,30 +1,38 @@
|
|||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neode } from '../../bootstrap/neo4j'
|
||||||
|
import Resolver from './helpers/Resolver'
|
||||||
|
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
CreateSocialMedia: async (object, params, context, resolveInfo) => {
|
CreateSocialMedia: async (object, params, context, resolveInfo) => {
|
||||||
/**
|
const [user, socialMedia] = await Promise.all([
|
||||||
* TODO?: Creates double Nodes!
|
instance.find('User', context.user.id),
|
||||||
*/
|
instance.create('SocialMedia', params),
|
||||||
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
|
])
|
||||||
const session = context.driver.session()
|
await socialMedia.relateTo(user, 'ownedBy')
|
||||||
await session.run(
|
const response = await socialMedia.toJson()
|
||||||
`MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId})
|
|
||||||
MERGE (socialMedia)<-[:OWNED]-(owner)
|
|
||||||
RETURN owner`,
|
|
||||||
{
|
|
||||||
userId: context.user.id,
|
|
||||||
socialMediaId: socialMedia.id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
return socialMedia
|
return response
|
||||||
},
|
},
|
||||||
DeleteSocialMedia: async (object, params, context, resolveInfo) => {
|
UpdateSocialMedia: async (object, params, context, resolveInfo) => {
|
||||||
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
|
const socialMedia = await instance.find('SocialMedia', params.id)
|
||||||
|
await socialMedia.update({ url: params.url })
|
||||||
|
const response = await socialMedia.toJson()
|
||||||
|
|
||||||
return socialMedia
|
return response
|
||||||
|
},
|
||||||
|
DeleteSocialMedia: async (object, { id }, context, resolveInfo) => {
|
||||||
|
const socialMedia = await instance.find('SocialMedia', id)
|
||||||
|
if (!socialMedia) return null
|
||||||
|
await socialMedia.delete()
|
||||||
|
return socialMedia.toJson()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SocialMedia: Resolver('SocialMedia', {
|
||||||
|
idAttribute: 'url',
|
||||||
|
hasOne: {
|
||||||
|
ownedBy: '<-[:OWNED_BY]-(related:User)',
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,70 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer from '../../server'
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
import { host, login, gql } from '../../jest/helpers'
|
import { gql } from '../../jest/helpers'
|
||||||
|
import { neode, getDriver } from '../../bootstrap/neo4j'
|
||||||
|
|
||||||
|
const driver = getDriver()
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
describe('SocialMedia', () => {
|
describe('SocialMedia', () => {
|
||||||
let client
|
let socialMediaAction, someUser, ownerNode, owner
|
||||||
let headers
|
|
||||||
const mutationC = gql`
|
const ownerParams = {
|
||||||
|
email: 'pippi@example.com',
|
||||||
|
password: '1234',
|
||||||
|
name: 'Pippi Langstrumpf',
|
||||||
|
}
|
||||||
|
|
||||||
|
const userParams = {
|
||||||
|
email: 'kalle@example.com',
|
||||||
|
password: 'abcd',
|
||||||
|
name: 'Kalle Blomqvist',
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = 'https://twitter.com/pippi-langstrumpf'
|
||||||
|
const newUrl = 'https://twitter.com/bullerby'
|
||||||
|
|
||||||
|
const setUpSocialMedia = async () => {
|
||||||
|
const socialMediaNode = await instance.create('SocialMedia', { url })
|
||||||
|
await socialMediaNode.relateTo(ownerNode, 'ownedBy')
|
||||||
|
return socialMediaNode.toJson()
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const someUserNode = await instance.create('User', userParams)
|
||||||
|
someUser = await someUserNode.toJson()
|
||||||
|
ownerNode = await instance.create('User', ownerParams)
|
||||||
|
owner = await ownerNode.toJson()
|
||||||
|
|
||||||
|
socialMediaAction = async (user, mutation, variables) => {
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
driver,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { mutate } = createTestClient(server)
|
||||||
|
|
||||||
|
return mutate({
|
||||||
|
mutation,
|
||||||
|
variables,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('create social media', () => {
|
||||||
|
let mutation, variables
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mutation = gql`
|
||||||
mutation($url: String!) {
|
mutation($url: String!) {
|
||||||
CreateSocialMedia(url: $url) {
|
CreateSocialMedia(url: $url) {
|
||||||
id
|
id
|
||||||
@ -15,7 +72,154 @@ describe('SocialMedia', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const mutationD = gql`
|
variables = { url }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
const user = null
|
||||||
|
const result = await socialMediaAction(user, mutation, variables)
|
||||||
|
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
let user
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = owner
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates social media with the given url', async () => {
|
||||||
|
await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
CreateSocialMedia: {
|
||||||
|
id: expect.any(String),
|
||||||
|
url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects an empty string as url', async () => {
|
||||||
|
variables = { url: '' }
|
||||||
|
const result = await socialMediaAction(user, mutation, variables)
|
||||||
|
|
||||||
|
expect(result.errors[0].message).toEqual(
|
||||||
|
expect.stringContaining('"url" is not allowed to be empty'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects invalid urls', async () => {
|
||||||
|
variables = { url: 'not-a-url' }
|
||||||
|
const result = await socialMediaAction(user, mutation, variables)
|
||||||
|
|
||||||
|
expect(result.errors[0].message).toEqual(
|
||||||
|
expect.stringContaining('"url" must be a valid uri'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ownedBy', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mutation = gql`
|
||||||
|
mutation($url: String!) {
|
||||||
|
CreateSocialMedia(url: $url) {
|
||||||
|
url
|
||||||
|
ownedBy {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves', async () => {
|
||||||
|
const user = someUser
|
||||||
|
await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
CreateSocialMedia: { url, ownedBy: { name: 'Kalle Blomqvist' } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('update social media', () => {
|
||||||
|
let mutation, variables
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const socialMedia = await setUpSocialMedia()
|
||||||
|
|
||||||
|
mutation = gql`
|
||||||
|
mutation($id: ID!, $url: String!) {
|
||||||
|
UpdateSocialMedia(id: $id, url: $url) {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
variables = { url: newUrl, id: socialMedia.id }
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
const user = null
|
||||||
|
const result = await socialMediaAction(user, mutation, variables)
|
||||||
|
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as other user', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
const user = someUser
|
||||||
|
const result = await socialMediaAction(user, mutation, variables)
|
||||||
|
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as owner', () => {
|
||||||
|
let user
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = owner
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates social media with the given id', async () => {
|
||||||
|
const expected = {
|
||||||
|
data: {
|
||||||
|
UpdateSocialMedia: { ...variables },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual(
|
||||||
|
expect.objectContaining(expected),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not update if the the given id does not exist', async () => {
|
||||||
|
variables.id = 'some-id'
|
||||||
|
const result = await socialMediaAction(user, mutation, variables)
|
||||||
|
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete social media', () => {
|
||||||
|
let mutation, variables
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const socialMedia = await setUpSocialMedia()
|
||||||
|
|
||||||
|
mutation = gql`
|
||||||
mutation($id: ID!) {
|
mutation($id: ID!) {
|
||||||
DeleteSocialMedia(id: $id) {
|
DeleteSocialMedia(id: $id) {
|
||||||
id
|
id
|
||||||
@ -23,93 +227,48 @@ describe('SocialMedia', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
beforeEach(async () => {
|
variables = { url: newUrl, id: socialMedia.id }
|
||||||
await factory.create('User', {
|
|
||||||
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg',
|
|
||||||
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
|
|
||||||
name: 'Matilde Hermiston',
|
|
||||||
slug: 'matilde-hermiston',
|
|
||||||
role: 'user',
|
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await factory.cleanDatabase()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('throws authorization error', async () => {
|
it('throws authorization error', async () => {
|
||||||
client = new GraphQLClient(host)
|
const user = null
|
||||||
const variables = {
|
const result = await socialMediaAction(user, mutation, variables)
|
||||||
url: 'http://nsosp.org',
|
|
||||||
}
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
await expect(client.request(mutationC, variables)).rejects.toThrow('Not Authorised')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated as other user', () => {
|
||||||
|
it('throws authorization error', async () => {
|
||||||
|
const user = someUser
|
||||||
|
const result = await socialMediaAction(user, mutation, variables)
|
||||||
|
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as owner', () => {
|
||||||
|
let user
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login({
|
user = owner
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
client = new GraphQLClient(host, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates social media with correct URL', async () => {
|
it('deletes social media with the given id', 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 = {
|
const expected = {
|
||||||
|
data: {
|
||||||
DeleteSocialMedia: {
|
DeleteSocialMedia: {
|
||||||
id: id,
|
id: variables.id,
|
||||||
url: 'http://nsosp.org',
|
url,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await expect(client.request(mutationD, deletionVariables)).resolves.toEqual(expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rejects empty string', async () => {
|
await expect(socialMediaAction(user, mutation, variables)).resolves.toEqual(
|
||||||
const variables = {
|
expect.objectContaining(expected),
|
||||||
url: '',
|
|
||||||
}
|
|
||||||
await expect(client.request(mutationC, variables)).rejects.toThrow(
|
|
||||||
'"url" is not allowed to be empty',
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('validates URLs', async () => {
|
|
||||||
const variables = {
|
|
||||||
url: 'not-a-url',
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(client.request(mutationC, variables)).rejects.toThrow(
|
|
||||||
'"url" must be a valid uri',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,57 +2,10 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
|||||||
import fileUpload from './fileUpload'
|
import fileUpload from './fileUpload'
|
||||||
import { neode } from '../../bootstrap/neo4j'
|
import { neode } from '../../bootstrap/neo4j'
|
||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
import { undefinedToNull } from '../helpers'
|
import Resolver from './helpers/Resolver'
|
||||||
|
|
||||||
const instance = neode()
|
const instance = neode()
|
||||||
|
|
||||||
const _has = (resolvers, { key, connection }, { returnType }) => {
|
|
||||||
return async (parent, params, context, resolveInfo) => {
|
|
||||||
if (typeof parent[key] !== 'undefined') return parent[key]
|
|
||||||
const { id } = parent
|
|
||||||
const statement = `MATCH(u:User {id: {id}})${connection} RETURN related`
|
|
||||||
const result = await instance.cypher(statement, { id })
|
|
||||||
let response = result.records.map(r => r.get('related').properties)
|
|
||||||
if (returnType === 'object') response = response[0] || null
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = obj => {
|
|
||||||
const resolvers = {}
|
|
||||||
for (const [key, connection] of Object.entries(obj)) {
|
|
||||||
resolvers[key] = async (parent, params, context, resolveInfo) => {
|
|
||||||
if (typeof parent[key] !== 'undefined') return parent[key]
|
|
||||||
const { id } = parent
|
|
||||||
const statement = `
|
|
||||||
MATCH(u:User {id: {id}})${connection}
|
|
||||||
WHERE NOT related.deleted = true AND NOT related.disabled = true
|
|
||||||
RETURN COUNT(DISTINCT(related)) as count
|
|
||||||
`
|
|
||||||
const result = await instance.cypher(statement, { id })
|
|
||||||
const [response] = result.records.map(r => r.get('count').toNumber())
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hasMany = obj => {
|
|
||||||
const resolvers = {}
|
|
||||||
for (const [key, connection] of Object.entries(obj)) {
|
|
||||||
resolvers[key] = _has(resolvers, { key, connection }, { returnType: 'iterable' })
|
|
||||||
}
|
|
||||||
return resolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
export const hasOne = obj => {
|
|
||||||
const resolvers = {}
|
|
||||||
for (const [key, connection] of Object.entries(obj)) {
|
|
||||||
resolvers[key] = _has(resolvers, { key, connection }, { returnType: 'object' })
|
|
||||||
}
|
|
||||||
return resolvers
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
User: async (object, args, context, resolveInfo) => {
|
User: async (object, args, context, resolveInfo) => {
|
||||||
@ -110,7 +63,8 @@ export default {
|
|||||||
let [{ email }] = result.records.map(r => r.get('e').properties)
|
let [{ email }] = result.records.map(r => r.get('e').properties)
|
||||||
return email
|
return email
|
||||||
},
|
},
|
||||||
...undefinedToNull([
|
...Resolver('User', {
|
||||||
|
undefinedToNull: [
|
||||||
'actorId',
|
'actorId',
|
||||||
'avatar',
|
'avatar',
|
||||||
'coverImg',
|
'coverImg',
|
||||||
@ -118,8 +72,8 @@ export default {
|
|||||||
'disabled',
|
'disabled',
|
||||||
'locationName',
|
'locationName',
|
||||||
'about',
|
'about',
|
||||||
]),
|
],
|
||||||
...count({
|
count: {
|
||||||
contributionsCount: '-[:WROTE]->(related:Post)',
|
contributionsCount: '-[:WROTE]->(related:Post)',
|
||||||
friendsCount: '<-[:FRIENDS]->(related:User)',
|
friendsCount: '<-[:FRIENDS]->(related:User)',
|
||||||
followingCount: '-[:FOLLOWS]->(related:User)',
|
followingCount: '-[:FOLLOWS]->(related:User)',
|
||||||
@ -128,17 +82,17 @@ export default {
|
|||||||
commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)',
|
commentedCount: '-[:WROTE]->(:Comment)-[:COMMENTS]->(related:Post)',
|
||||||
shoutedCount: '-[:SHOUTED]->(related:Post)',
|
shoutedCount: '-[:SHOUTED]->(related:Post)',
|
||||||
badgesCount: '<-[:REWARDED]-(related:Badge)',
|
badgesCount: '<-[:REWARDED]-(related:Badge)',
|
||||||
}),
|
},
|
||||||
...hasOne({
|
hasOne: {
|
||||||
invitedBy: '<-[:INVITED]-(related:User)',
|
invitedBy: '<-[:INVITED]-(related:User)',
|
||||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||||
}),
|
},
|
||||||
...hasMany({
|
hasMany: {
|
||||||
followedBy: '<-[:FOLLOWS]-(related:User)',
|
followedBy: '<-[:FOLLOWS]-(related:User)',
|
||||||
following: '-[:FOLLOWS]->(related:User)',
|
following: '-[:FOLLOWS]->(related:User)',
|
||||||
friends: '-[:FRIENDS]-(related:User)',
|
friends: '-[:FRIENDS]-(related:User)',
|
||||||
blacklisted: '-[:BLACKLISTED]->(related:User)',
|
blacklisted: '-[:BLACKLISTED]->(related:User)',
|
||||||
socialMedia: '-[:OWNED]->(related:SocialMedia)',
|
socialMedia: '-[:OWNED_BY]->(related:SocialMedia',
|
||||||
contributions: '-[:WROTE]->(related:Post)',
|
contributions: '-[:WROTE]->(related:Post)',
|
||||||
comments: '-[:WROTE]->(related:Comment)',
|
comments: '-[:WROTE]->(related:Comment)',
|
||||||
shouted: '-[:SHOUTED]->(related:Post)',
|
shouted: '-[:SHOUTED]->(related:Post)',
|
||||||
@ -146,6 +100,7 @@ export default {
|
|||||||
organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)',
|
organizationsOwned: '-[:OWNING_ORGA]->(related:Organization)',
|
||||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||||
badges: '<-[:REWARDED]-(related:Badge)',
|
badges: '<-[:REWARDED]-(related:Badge)',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,8 +131,3 @@ type SharedInboxEndpoint {
|
|||||||
uri: String
|
uri: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocialMedia {
|
|
||||||
id: ID!
|
|
||||||
url: String
|
|
||||||
ownedBy: [User]! @relation(name: "OWNED", direction: "IN")
|
|
||||||
}
|
|
||||||
|
|||||||
11
backend/src/schema/types/type/SocialMedia.gql
Normal file
11
backend/src/schema/types/type/SocialMedia.gql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
type SocialMedia {
|
||||||
|
id: ID!
|
||||||
|
url: String
|
||||||
|
ownedBy: User! @relation(name: "OWNED_BY", direction: "IN")
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
CreateSocialMedia(id: ID, url: String!): SocialMedia
|
||||||
|
UpdateSocialMedia(id: ID!, url: String!): SocialMedia
|
||||||
|
DeleteSocialMedia(id: ID!): SocialMedia
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ type User {
|
|||||||
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
location: Location @cypher(statement: "MATCH (this)-[:IS_IN]->(l:Location) RETURN l")
|
||||||
locationName: String
|
locationName: String
|
||||||
about: String
|
about: String
|
||||||
socialMedia: [SocialMedia]! @relation(name: "OWNED", direction: "OUT")
|
socialMedia: [SocialMedia]! @relation(name: "OWNED_BY", direction: "OUT")
|
||||||
|
|
||||||
#createdAt: DateTime
|
#createdAt: DateTime
|
||||||
#updatedAt: DateTime
|
#updatedAt: DateTime
|
||||||
|
|||||||
@ -1583,10 +1583,10 @@ apollo-server-caching@0.5.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^5.0.0"
|
lru-cache "^5.0.0"
|
||||||
|
|
||||||
apollo-server-core@2.7.2:
|
apollo-server-core@2.8.0:
|
||||||
version "2.7.2"
|
version "2.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.7.2.tgz#4acd9f4d0d235bef0e596e2a821326dfc07ae7b2"
|
resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.8.0.tgz#0bfba3d5eb557c6ffa68ad60e77f69e2634e211d"
|
||||||
integrity sha512-Dv6ZMMf8Y+ovkj1ioMtcYvjbcsSMqnZblbPPzOWo29vvKEjMXAL1OTSL1WBYxGA/WSBSCTnxAzipn71XZkYoCw==
|
integrity sha512-Bilaaaol8c4mpF+8DatsAm+leKd0lbz1jS7M+WIuu8GscAXFzzfT6311dNC7zx0wT5FUNNdHdvQOry/lyCn5GA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@apollographql/apollo-tools" "^0.4.0"
|
"@apollographql/apollo-tools" "^0.4.0"
|
||||||
"@apollographql/graphql-playground-html" "1.6.24"
|
"@apollographql/graphql-playground-html" "1.6.24"
|
||||||
@ -1601,7 +1601,7 @@ apollo-server-core@2.7.2:
|
|||||||
apollo-server-types "0.2.1"
|
apollo-server-types "0.2.1"
|
||||||
apollo-tracing "0.8.1"
|
apollo-tracing "0.8.1"
|
||||||
fast-json-stable-stringify "^2.0.0"
|
fast-json-stable-stringify "^2.0.0"
|
||||||
graphql-extensions "0.8.2"
|
graphql-extensions "0.9.0"
|
||||||
graphql-tag "^2.9.2"
|
graphql-tag "^2.9.2"
|
||||||
graphql-tools "^4.0.0"
|
graphql-tools "^4.0.0"
|
||||||
graphql-upload "^8.0.2"
|
graphql-upload "^8.0.2"
|
||||||
@ -1622,10 +1622,10 @@ apollo-server-errors@2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.1.tgz#033cf331463ebb99a563f8354180b41ac6714eb6"
|
resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.1.tgz#033cf331463ebb99a563f8354180b41ac6714eb6"
|
||||||
integrity sha512-errZvnh0vUQChecT7M4A/h94dnBSRL213dNxpM5ueMypaLYgnp4hiCTWIEaooo9E4yMGd1qA6WaNbLDG2+bjcg==
|
integrity sha512-errZvnh0vUQChecT7M4A/h94dnBSRL213dNxpM5ueMypaLYgnp4hiCTWIEaooo9E4yMGd1qA6WaNbLDG2+bjcg==
|
||||||
|
|
||||||
apollo-server-express@2.7.2, apollo-server-express@^2.7.2:
|
apollo-server-express@2.8.0, apollo-server-express@^2.7.2:
|
||||||
version "2.7.2"
|
version "2.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.7.2.tgz#a6b9514f42463c9514d2dda34e07ee240b73f764"
|
resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.8.0.tgz#3815eee2fccfc9cba6d232420fa7411cda062647"
|
||||||
integrity sha512-XW+MTKyjJDrHqeLJt9Z3OzLTCRxp53XzVVhF0f/Bs9GCODPlTiBaoiMwY2mXQ7WqK6gkYAH1kRp7d/psPFKE5w==
|
integrity sha512-7dj4CVyOMz1HeVoF8nw3aKw7QV/5D6PACiweu6k9xPRHurYf0bj3ncYkAMPNnxIAwu1I8FzMn4/84BWoKJ7ZFg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@apollographql/graphql-playground-html" "1.6.24"
|
"@apollographql/graphql-playground-html" "1.6.24"
|
||||||
"@types/accepts" "^1.3.5"
|
"@types/accepts" "^1.3.5"
|
||||||
@ -1633,7 +1633,7 @@ apollo-server-express@2.7.2, apollo-server-express@^2.7.2:
|
|||||||
"@types/cors" "^2.8.4"
|
"@types/cors" "^2.8.4"
|
||||||
"@types/express" "4.17.0"
|
"@types/express" "4.17.0"
|
||||||
accepts "^1.3.5"
|
accepts "^1.3.5"
|
||||||
apollo-server-core "2.7.2"
|
apollo-server-core "2.8.0"
|
||||||
apollo-server-types "0.2.1"
|
apollo-server-types "0.2.1"
|
||||||
body-parser "^1.18.3"
|
body-parser "^1.18.3"
|
||||||
cors "^2.8.4"
|
cors "^2.8.4"
|
||||||
@ -1649,12 +1649,12 @@ apollo-server-plugin-base@0.6.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-types "0.2.1"
|
apollo-server-types "0.2.1"
|
||||||
|
|
||||||
apollo-server-testing@~2.7.2:
|
apollo-server-testing@~2.8.0:
|
||||||
version "2.7.2"
|
version "2.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.7.2.tgz#c0fb18fa7eef0c945c5b73887d19c704dac5957e"
|
resolved "https://registry.yarnpkg.com/apollo-server-testing/-/apollo-server-testing-2.8.0.tgz#57c31575d51d13f09b5a14709c482b9d5986cf58"
|
||||||
integrity sha512-fjWJ6K5t3xilPrXg5rQtqFZN0JbNSthkNyJb4Qfpdj9WA5r0vZCeARAFcIrv7o3pSBstyy1UBvJuNG0Rw6HTzA==
|
integrity sha512-a+9OZcqNeeUkOGVDOfuSmrXsTu3LnG9NvfM/4H2XJBJWHzghiuU6xZV2yHetZSTLXsAvWw3To2j1g8+/A8Yqsg==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-core "2.7.2"
|
apollo-server-core "2.8.0"
|
||||||
|
|
||||||
apollo-server-types@0.2.1:
|
apollo-server-types@0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
@ -1665,13 +1665,13 @@ apollo-server-types@0.2.1:
|
|||||||
apollo-server-caching "0.5.0"
|
apollo-server-caching "0.5.0"
|
||||||
apollo-server-env "2.4.1"
|
apollo-server-env "2.4.1"
|
||||||
|
|
||||||
apollo-server@~2.7.2:
|
apollo-server@~2.8.0:
|
||||||
version "2.7.2"
|
version "2.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.7.2.tgz#a3eeb6916f11802502ab40819e9f06a4c553c84a"
|
resolved "https://registry.yarnpkg.com/apollo-server/-/apollo-server-2.8.0.tgz#c57261f02f9f2865778ad8e0cdb3c6a80307beb5"
|
||||||
integrity sha512-0FkNi2ViLJoTglTuBTZ8OeUSK2/LOk4sMGmojDYUYkyVuM5lZX+GWVf3pDNvhrnC2po6TkntkNL4EJLXfKwNMA==
|
integrity sha512-WtHbP8/C7WkFBCA44V2uTiyuefgqlVSAb6di4XcCPLyopcg9XGKHYRPyp5uOOKlMDTfryNqV59DWHn5/oXkZmQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-server-core "2.7.2"
|
apollo-server-core "2.8.0"
|
||||||
apollo-server-express "2.7.2"
|
apollo-server-express "2.8.0"
|
||||||
express "^4.0.0"
|
express "^4.0.0"
|
||||||
graphql-subscriptions "^1.0.0"
|
graphql-subscriptions "^1.0.0"
|
||||||
graphql-tools "^4.0.0"
|
graphql-tools "^4.0.0"
|
||||||
@ -3994,6 +3994,15 @@ graphql-extensions@0.8.2:
|
|||||||
apollo-server-env "2.4.1"
|
apollo-server-env "2.4.1"
|
||||||
apollo-server-types "0.2.1"
|
apollo-server-types "0.2.1"
|
||||||
|
|
||||||
|
graphql-extensions@0.9.0:
|
||||||
|
version "0.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.9.0.tgz#88fb3b161f84a92f4a9032b2941919113600635d"
|
||||||
|
integrity sha512-0GQjQ2t2Nkg9OIk3eS5jcvQLzFkJtVB73t4AnEl7bejPwwShtY37XzE7mOlfof1OqbvRKvKFoks+wSjus2Fhzw==
|
||||||
|
dependencies:
|
||||||
|
"@apollographql/apollo-tools" "^0.4.0"
|
||||||
|
apollo-server-env "2.4.1"
|
||||||
|
apollo-server-types "0.2.1"
|
||||||
|
|
||||||
graphql-import@0.7.1:
|
graphql-import@0.7.1:
|
||||||
version "0.7.1"
|
version "0.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223"
|
resolved "https://registry.yarnpkg.com/graphql-import/-/graphql-import-0.7.1.tgz#4add8d91a5f752d764b0a4a7a461fcd93136f223"
|
||||||
@ -4007,12 +4016,12 @@ graphql-iso-date@~3.6.1:
|
|||||||
resolved "https://registry.yarnpkg.com/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz#bd2d0dc886e0f954cbbbc496bbf1d480b57ffa96"
|
resolved "https://registry.yarnpkg.com/graphql-iso-date/-/graphql-iso-date-3.6.1.tgz#bd2d0dc886e0f954cbbbc496bbf1d480b57ffa96"
|
||||||
integrity sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q==
|
integrity sha512-AwFGIuYMJQXOEAgRlJlFL4H1ncFM8n8XmoVDTNypNOZyQ8LFDG2ppMFlsS862BSTCDcSUfHp8PD3/uJhv7t59Q==
|
||||||
|
|
||||||
graphql-middleware@~3.0.2:
|
graphql-middleware@~3.0.3:
|
||||||
version "3.0.2"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-3.0.2.tgz#c8cdb67615eec02aec237b455e679f5fc973ddc4"
|
resolved "https://registry.yarnpkg.com/graphql-middleware/-/graphql-middleware-3.0.3.tgz#58cbce80892fb933d72794447f33f978fc743aa5"
|
||||||
integrity sha512-sRqu1sF+77z42z1OVM1QDHKQWnWY5K3nAgqWiZwx3U4tqNZprrDuXxSChPMliV343IrVkpYdejUYq9w24Ot3FA==
|
integrity sha512-Os8Vt25MqqwIPJUCCcHznzs6EqarGmM0kkNPUiDnMEkX6vqjA+HugCWatinP+7+fqBqecFUsJmoL4ZypdqZZkg==
|
||||||
dependencies:
|
dependencies:
|
||||||
graphql-tools "^4.0.4"
|
graphql-tools "^4.0.5"
|
||||||
|
|
||||||
graphql-request@~1.8.2:
|
graphql-request@~1.8.2:
|
||||||
version "1.8.2"
|
version "1.8.2"
|
||||||
@ -4062,10 +4071,10 @@ graphql-toolkit@0.4.1:
|
|||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
valid-url "1.0.9"
|
valid-url "1.0.9"
|
||||||
|
|
||||||
graphql-tools@^4.0.0, graphql-tools@^4.0.4:
|
graphql-tools@^4.0.0, graphql-tools@^4.0.4, graphql-tools@^4.0.5:
|
||||||
version "4.0.4"
|
version "4.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.4.tgz#ca08a63454221fdde825fe45fbd315eb2a6d566b"
|
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.5.tgz#d2b41ee0a330bfef833e5cdae7e1f0b0d86b1754"
|
||||||
integrity sha512-chF12etTIGVVGy3fCTJ1ivJX2KB7OSG4c6UOJQuqOHCmBQwTyNgCDuejZKvpYxNZiEx7bwIjrodDgDe9RIkjlw==
|
integrity sha512-kQCh3IZsMqquDx7zfIGWBau42xe46gmqabwYkpPlCLIjcEY1XK+auP7iGRD9/205BPyoQdY8hT96MPpgERdC9Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
apollo-link "^1.2.3"
|
apollo-link "^1.2.3"
|
||||||
apollo-utilities "^1.0.1"
|
apollo-utilities "^1.0.1"
|
||||||
|
|||||||
@ -1,73 +1,70 @@
|
|||||||
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
|
import { When, Then } from "cypress-cucumber-preprocessor/steps";
|
||||||
When('I search for {string}', value => {
|
When("I search for {string}", value => {
|
||||||
cy.get('#nav-search')
|
cy.get("#nav-search")
|
||||||
.focus()
|
.focus()
|
||||||
.type(value)
|
.type(value);
|
||||||
})
|
});
|
||||||
|
|
||||||
Then('I should have one post in the select dropdown', () => {
|
Then("I should have one post in the select dropdown", () => {
|
||||||
cy.get('.ds-select-dropdown').should($li => {
|
cy.get(".input .ds-select-dropdown").should($li => {
|
||||||
expect($li).to.have.length(1)
|
expect($li).to.have.length(1);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
Then('I should see the following posts in the select dropdown:', table => {
|
Then("I should see the following posts in the select dropdown:", table => {
|
||||||
table.hashes().forEach(({ title }) => {
|
table.hashes().forEach(({ title }) => {
|
||||||
cy.get('.ds-select-dropdown').should('contain', title)
|
cy.get(".ds-select-dropdown").should("contain", title);
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
When('I type {string} and press Enter', value => {
|
When("I type {string} and press Enter", value => {
|
||||||
cy.get('#nav-search')
|
cy.get("#nav-search")
|
||||||
.focus()
|
.focus()
|
||||||
.type(value)
|
.type(value)
|
||||||
.type('{enter}', { force: true })
|
.type("{enter}", { force: true });
|
||||||
})
|
});
|
||||||
|
|
||||||
When('I type {string} and press escape', value => {
|
When("I type {string} and press escape", value => {
|
||||||
cy.get('#nav-search')
|
cy.get("#nav-search")
|
||||||
.focus()
|
.focus()
|
||||||
.type(value)
|
.type(value)
|
||||||
.type('{esc}')
|
.type("{esc}");
|
||||||
})
|
});
|
||||||
|
|
||||||
Then('the search field should clear', () => {
|
Then("the search field should clear", () => {
|
||||||
cy.get('#nav-search').should('have.text', '')
|
cy.get("#nav-search").should("have.text", "");
|
||||||
})
|
});
|
||||||
|
|
||||||
When('I select an entry', () => {
|
When("I select an entry", () => {
|
||||||
cy.get('.ds-select-dropdown ul li')
|
cy.get(".input .ds-select-dropdown ul li")
|
||||||
.first()
|
.first()
|
||||||
.trigger('click')
|
.trigger("click");
|
||||||
})
|
});
|
||||||
|
|
||||||
Then("I should be on the post's page", () => {
|
Then("I should be on the post's page", () => {
|
||||||
cy.location('pathname').should(
|
cy.location("pathname").should("contain", "/post/");
|
||||||
'contain',
|
cy.location("pathname").should(
|
||||||
'/post/'
|
"eq",
|
||||||
)
|
"/post/p1/101-essays-that-will-change-the-way-you-think"
|
||||||
cy.location('pathname').should(
|
);
|
||||||
'eq',
|
});
|
||||||
'/post/p1/101-essays-that-will-change-the-way-you-think'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
Then(
|
Then(
|
||||||
'I should see posts with the searched-for term in the select dropdown',
|
"I should see posts with the searched-for term in the select dropdown",
|
||||||
() => {
|
() => {
|
||||||
cy.get('.ds-select-dropdown').should(
|
cy.get(".ds-select-dropdown").should(
|
||||||
'contain',
|
"contain",
|
||||||
'101 Essays that will change the way you think'
|
"101 Essays that will change the way you think"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
Then(
|
Then(
|
||||||
'I should not see posts without the searched-for term in the select dropdown',
|
"I should not see posts without the searched-for term in the select dropdown",
|
||||||
() => {
|
() => {
|
||||||
cy.get('.ds-select-dropdown').should(
|
cy.get(".ds-select-dropdown").should(
|
||||||
'not.contain',
|
"not.contain",
|
||||||
'No searched for content'
|
"No searched for content"
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|||||||
@ -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)
|
||||||
|
})
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -14,6 +14,8 @@ localVue.use(Styleguide)
|
|||||||
localVue.use(Filters)
|
localVue.use(Filters)
|
||||||
|
|
||||||
config.stubs['no-ssr'] = '<span><slot /></span>'
|
config.stubs['no-ssr'] = '<span><slot /></span>'
|
||||||
|
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||||
|
config.stubs['v-popover'] = '<span><slot /></span>'
|
||||||
|
|
||||||
describe('ContributionForm.vue', () => {
|
describe('ContributionForm.vue', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
@ -24,9 +26,22 @@ describe('ContributionForm.vue', () => {
|
|||||||
let mocks
|
let mocks
|
||||||
let propsData
|
let propsData
|
||||||
const postTitle = 'this is a title for a post'
|
const postTitle = 'this is a title for a post'
|
||||||
|
const postTitleTooShort = 'xx'
|
||||||
|
let postTitleTooLong = ''
|
||||||
|
for (let i = 0; i < 65; i++) {
|
||||||
|
postTitleTooLong += 'x'
|
||||||
|
}
|
||||||
const postContent = 'this is a post'
|
const postContent = 'this is a post'
|
||||||
|
const postContentTooShort = 'xx'
|
||||||
|
let postContentTooLong = ''
|
||||||
|
for (let i = 0; i < 2001; i++) {
|
||||||
|
postContentTooLong += 'x'
|
||||||
|
}
|
||||||
const imageUpload = {
|
const imageUpload = {
|
||||||
file: { filename: 'avataar.svg', previewElement: '' },
|
file: {
|
||||||
|
filename: 'avataar.svg',
|
||||||
|
previewElement: '',
|
||||||
|
},
|
||||||
url: 'someUrlToImage',
|
url: 'someUrlToImage',
|
||||||
}
|
}
|
||||||
const image = '/uploads/1562010976466-avataaars'
|
const image = '/uploads/1562010976466-avataaars'
|
||||||
@ -34,9 +49,7 @@ describe('ContributionForm.vue', () => {
|
|||||||
mocks = {
|
mocks = {
|
||||||
$t: jest.fn(),
|
$t: jest.fn(),
|
||||||
$apollo: {
|
$apollo: {
|
||||||
mutate: jest
|
mutate: jest.fn().mockResolvedValueOnce({
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
data: {
|
data: {
|
||||||
CreatePost: {
|
CreatePost: {
|
||||||
title: postTitle,
|
title: postTitle,
|
||||||
@ -46,9 +59,6 @@ describe('ContributionForm.vue', () => {
|
|||||||
language: 'en',
|
language: 'en',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
|
||||||
.mockRejectedValue({
|
|
||||||
message: 'Not Authorised!',
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
$toast: {
|
$toast: {
|
||||||
@ -71,6 +81,13 @@ describe('ContributionForm.vue', () => {
|
|||||||
'editor/placeholder': () => {
|
'editor/placeholder': () => {
|
||||||
return 'some cool placeholder'
|
return 'some cool placeholder'
|
||||||
},
|
},
|
||||||
|
'auth/user': () => {
|
||||||
|
return {
|
||||||
|
id: '4711',
|
||||||
|
name: 'You yourself',
|
||||||
|
slug: 'you-yourself',
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
getters,
|
getters,
|
||||||
@ -106,16 +123,53 @@ describe('ContributionForm.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('invalid form submission', () => {
|
describe('invalid form submission', () => {
|
||||||
it('title required for form submission', async () => {
|
it('title and content should not be empty ', async () => {
|
||||||
postTitleInput = wrapper.find('.ds-input')
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
postTitleInput.setValue(postTitle)
|
|
||||||
await wrapper.find('form').trigger('submit')
|
|
||||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('content required for form submission', async () => {
|
it('title should not be empty', async () => {
|
||||||
wrapper.vm.updateEditorContent(postContent)
|
await wrapper.vm.updateEditorContent(postContent)
|
||||||
await wrapper.find('form').trigger('submit')
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('title should not be too long', async () => {
|
||||||
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
|
postTitleInput.setValue(postTitleTooLong)
|
||||||
|
await wrapper.vm.updateEditorContent(postContent)
|
||||||
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('title should not be too short', async () => {
|
||||||
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
|
postTitleInput.setValue(postTitleTooShort)
|
||||||
|
await wrapper.vm.updateEditorContent(postContent)
|
||||||
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('content should not be empty', async () => {
|
||||||
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
|
postTitleInput.setValue(postTitle)
|
||||||
|
await wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('content should not be too short', async () => {
|
||||||
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
|
postTitleInput.setValue(postTitle)
|
||||||
|
await wrapper.vm.updateEditorContent(postContentTooShort)
|
||||||
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('content should not be too long', async () => {
|
||||||
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
|
postTitleInput.setValue(postTitle)
|
||||||
|
await wrapper.vm.updateEditorContent(postContentTooLong)
|
||||||
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
expect(mocks.$apollo.mutate).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -136,15 +190,16 @@ describe('ContributionForm.vue', () => {
|
|||||||
}
|
}
|
||||||
postTitleInput = wrapper.find('.ds-input')
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
postTitleInput.setValue(postTitle)
|
postTitleInput.setValue(postTitle)
|
||||||
wrapper.vm.updateEditorContent(postContent)
|
await wrapper.vm.updateEditorContent(postContent)
|
||||||
await wrapper.find('form').trigger('submit')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with title and content', () => {
|
it('with title and content', () => {
|
||||||
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
|
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("sends a fallback language based on a user's locale", () => {
|
it("sends a fallback language based on a user's locale", () => {
|
||||||
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -152,7 +207,7 @@ describe('ContributionForm.vue', () => {
|
|||||||
expectedParams.variables.language = 'de'
|
expectedParams.variables.language = 'de'
|
||||||
deutschOption = wrapper.findAll('li').at(0)
|
deutschOption = wrapper.findAll('li').at(0)
|
||||||
deutschOption.trigger('click')
|
deutschOption.trigger('click')
|
||||||
await wrapper.find('form').trigger('submit')
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -160,22 +215,26 @@ describe('ContributionForm.vue', () => {
|
|||||||
const categoryIds = ['cat12', 'cat15', 'cat37']
|
const categoryIds = ['cat12', 'cat15', 'cat37']
|
||||||
expectedParams.variables.categoryIds = categoryIds
|
expectedParams.variables.categoryIds = categoryIds
|
||||||
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
|
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('supports adding a teaser image', async () => {
|
it('supports adding a teaser image', async () => {
|
||||||
expectedParams.variables.imageUpload = imageUpload
|
expectedParams.variables.imageUpload = imageUpload
|
||||||
wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload)
|
wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload)
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("pushes the user to the post's page", async () => {
|
it("pushes the user to the post's page", async () => {
|
||||||
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
|
await mocks.$apollo.mutate
|
||||||
expect(mocks.$router.push).toHaveBeenCalledTimes(1)
|
expect(mocks.$router.push).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows a success toaster', () => {
|
it('shows a success toaster', async () => {
|
||||||
|
wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
|
await mocks.$apollo.mutate
|
||||||
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
|
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -191,18 +250,19 @@ describe('ContributionForm.vue', () => {
|
|||||||
describe('handles errors', () => {
|
describe('handles errors', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
|
mocks.$apollo.mutate = jest.fn().mockRejectedValueOnce({
|
||||||
|
message: 'Not Authorised!',
|
||||||
|
})
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
postTitleInput = wrapper.find('.ds-input')
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
postTitleInput.setValue(postTitle)
|
postTitleInput.setValue(postTitle)
|
||||||
wrapper.vm.updateEditorContent(postContent)
|
await wrapper.vm.updateEditorContent(postContent)
|
||||||
// second submission causes mutation to reject
|
|
||||||
await wrapper.find('form').trigger('submit')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows an error toaster when apollo mutation rejects', async () => {
|
it('shows an error toaster when apollo mutation rejects', async () => {
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
await mocks.$apollo.mutate
|
await mocks.$apollo.mutate
|
||||||
expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!')
|
await expect(mocks.$toast.error).toHaveBeenCalledWith('Not Authorised!')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -217,7 +277,12 @@ describe('ContributionForm.vue', () => {
|
|||||||
content: 'auf Deutsch geschrieben',
|
content: 'auf Deutsch geschrieben',
|
||||||
language: 'de',
|
language: 'de',
|
||||||
image,
|
image,
|
||||||
categories: [{ id: 'cat12', name: 'Democracy & Politics' }],
|
categories: [
|
||||||
|
{
|
||||||
|
id: 'cat12',
|
||||||
|
name: 'Democracy & Politics',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
@ -255,7 +320,7 @@ describe('ContributionForm.vue', () => {
|
|||||||
postTitleInput = wrapper.find('.ds-input')
|
postTitleInput = wrapper.find('.ds-input')
|
||||||
postTitleInput.setValue(postTitle)
|
postTitleInput.setValue(postTitle)
|
||||||
wrapper.vm.updateEditorContent(postContent)
|
wrapper.vm.updateEditorContent(postContent)
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -266,7 +331,7 @@ describe('ContributionForm.vue', () => {
|
|||||||
wrapper.vm.updateEditorContent(postContent)
|
wrapper.vm.updateEditorContent(postContent)
|
||||||
expectedParams.variables.categoryIds = categoryIds
|
expectedParams.variables.categoryIds = categoryIds
|
||||||
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
|
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('.submit-button-for-test').trigger('click')
|
||||||
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-form ref="contributionForm" v-model="form" :schema="formSchema" @submit="submit">
|
<ds-form ref="contributionForm" v-model="form" :schema="formSchema">
|
||||||
<template slot-scope="{ errors }">
|
<template slot-scope="{ errors }">
|
||||||
<ds-card>
|
<ds-card>
|
||||||
<hc-teaser-image :contribution="contribution" @addTeaserImage="addTeaserImage">
|
<hc-teaser-image :contribution="contribution" @addTeaserImage="addTeaserImage">
|
||||||
@ -13,6 +13,7 @@
|
|||||||
<hc-user :user="currentUser" :trunc="35" />
|
<hc-user :user="currentUser" :trunc="35" />
|
||||||
<ds-space />
|
<ds-space />
|
||||||
<ds-input model="title" class="post-title" placeholder="Title" name="title" autofocus />
|
<ds-input model="title" class="post-title" placeholder="Title" name="title" autofocus />
|
||||||
|
<small class="smallTag">{{ form.title.length }}/{{ formSchema.title.max }}</small>
|
||||||
<no-ssr>
|
<no-ssr>
|
||||||
<hc-editor
|
<hc-editor
|
||||||
:users="users"
|
:users="users"
|
||||||
@ -20,6 +21,7 @@
|
|||||||
:value="form.content"
|
:value="form.content"
|
||||||
@input="updateEditorContent"
|
@input="updateEditorContent"
|
||||||
/>
|
/>
|
||||||
|
<small class="smallTag">{{ form.contentLength }}/{{ contentMax }}</small>
|
||||||
</no-ssr>
|
</no-ssr>
|
||||||
<ds-space margin-bottom="xxx-large" />
|
<ds-space margin-bottom="xxx-large" />
|
||||||
<hc-categories-select
|
<hc-categories-select
|
||||||
@ -44,18 +46,20 @@
|
|||||||
<div slot="footer" style="text-align: right">
|
<div slot="footer" style="text-align: right">
|
||||||
<ds-button
|
<ds-button
|
||||||
class="cancel-button"
|
class="cancel-button"
|
||||||
:disabled="loading || disabled"
|
:disabled="loading"
|
||||||
ghost
|
ghost
|
||||||
@click.prevent="$router.back()"
|
@click.prevent="$router.back()"
|
||||||
>
|
>
|
||||||
{{ $t('actions.cancel') }}
|
{{ $t('actions.cancel') }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
<ds-button
|
<ds-button
|
||||||
|
class="submit-button-for-test"
|
||||||
type="submit"
|
type="submit"
|
||||||
icon="check"
|
icon="check"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="disabled || errors"
|
:disabled="disabledByContent || errors"
|
||||||
primary
|
primary
|
||||||
|
@click.prevent="submit"
|
||||||
>
|
>
|
||||||
{{ $t('actions.save') }}
|
{{ $t('actions.save') }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
@ -92,6 +96,7 @@ export default {
|
|||||||
form: {
|
form: {
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
|
contentLength: 0,
|
||||||
teaserImage: null,
|
teaserImage: null,
|
||||||
image: null,
|
image: null,
|
||||||
language: null,
|
language: null,
|
||||||
@ -100,13 +105,16 @@ export default {
|
|||||||
},
|
},
|
||||||
formSchema: {
|
formSchema: {
|
||||||
title: { required: true, min: 3, max: 64 },
|
title: { required: true, min: 3, max: 64 },
|
||||||
content: { required: true, min: 3 },
|
content: [{ required: true }],
|
||||||
},
|
},
|
||||||
id: null,
|
id: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
disabled: false,
|
disabledByContent: true,
|
||||||
slug: null,
|
slug: null,
|
||||||
users: [],
|
users: [],
|
||||||
|
contentMin: 3,
|
||||||
|
contentMax: 2000,
|
||||||
|
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -119,8 +127,9 @@ export default {
|
|||||||
}
|
}
|
||||||
this.id = contribution.id
|
this.id = contribution.id
|
||||||
this.slug = contribution.slug
|
this.slug = contribution.slug
|
||||||
this.form.content = contribution.content
|
|
||||||
this.form.title = contribution.title
|
this.form.title = contribution.title
|
||||||
|
this.form.content = contribution.content
|
||||||
|
this.manageContent(this.form.content)
|
||||||
this.form.image = contribution.image
|
this.form.image = contribution.image
|
||||||
this.form.categoryIds = this.categoryIds(contribution.categories)
|
this.form.categoryIds = this.categoryIds(contribution.categories)
|
||||||
},
|
},
|
||||||
@ -169,7 +178,7 @@ export default {
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.$toast.success(this.$t('contribution.success'))
|
this.$toast.success(this.$t('contribution.success'))
|
||||||
this.disabled = true
|
this.disabledByContent = true
|
||||||
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
|
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
|
||||||
|
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
@ -180,12 +189,21 @@ export default {
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
this.$toast.error(err.message)
|
this.$toast.error(err.message)
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.disabled = false
|
this.disabledByContent = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateEditorContent(value) {
|
updateEditorContent(value) {
|
||||||
// this.form.content = value
|
// TODO: Do smth????? what is happening
|
||||||
this.$refs.contributionForm.update('content', value)
|
this.$refs.contributionForm.update('content', value)
|
||||||
|
this.manageContent(value)
|
||||||
|
},
|
||||||
|
manageContent(content) {
|
||||||
|
// filter HTML out of content value
|
||||||
|
const str = content.replace(/<\/?[^>]+(>|$)/gm, '')
|
||||||
|
// Set counter length of text
|
||||||
|
this.form.contentLength = str.length
|
||||||
|
// Enable save button if requirements are met
|
||||||
|
this.disabledByContent = !(this.contentMin <= str.length && str.length <= this.contentMax)
|
||||||
},
|
},
|
||||||
availableLocales() {
|
availableLocales() {
|
||||||
orderBy(locales, 'name').map(locale => {
|
orderBy(locales, 'name').map(locale => {
|
||||||
@ -242,6 +260,11 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.smallTag {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
left: 90%;
|
||||||
|
}
|
||||||
.post-title {
|
.post-title {
|
||||||
margin-top: $space-x-small;
|
margin-top: $space-x-small;
|
||||||
margin-bottom: $space-xx-small;
|
margin-bottom: $space-xx-small;
|
||||||
|
|||||||
@ -80,8 +80,8 @@ export default i18n => {
|
|||||||
export const filterPosts = i18n => {
|
export const filterPosts = i18n => {
|
||||||
const lang = i18n.locale().toUpperCase()
|
const lang = i18n.locale().toUpperCase()
|
||||||
return gql`
|
return gql`
|
||||||
query Post($filter: _PostFilter, $first: Int, $offset: Int) {
|
query Post($filter: _PostFilter, $first: Int, $offset: Int, $orderBy: [_PostOrdering]) {
|
||||||
Post(filter: $filter, first: $first, offset: $offset) {
|
Post(filter: $filter, first: $first, offset: $offset, orderBy: $orderBy) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
contentExcerpt
|
contentExcerpt
|
||||||
|
|||||||
@ -23,6 +23,12 @@
|
|||||||
"bank": "Bankverbindung",
|
"bank": "Bankverbindung",
|
||||||
"germany": "Deutschland"
|
"germany": "Deutschland"
|
||||||
},
|
},
|
||||||
|
"sorting": {
|
||||||
|
"newest": "Neuste",
|
||||||
|
"oldest": "Älteste",
|
||||||
|
"popular": "Beliebt",
|
||||||
|
"commented": "meist Kommentiert"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
|
"copy": "Wenn Du bereits ein Konto bei Human Connection hast, melde Dich bitte hier an.",
|
||||||
"login": "Einloggen",
|
"login": "Einloggen",
|
||||||
@ -168,7 +174,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 +293,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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,6 +23,12 @@
|
|||||||
"bank": "bank account",
|
"bank": "bank account",
|
||||||
"germany": "Germany"
|
"germany": "Germany"
|
||||||
},
|
},
|
||||||
|
"sorting": {
|
||||||
|
"newest": "Newest",
|
||||||
|
"oldest": "Oldest",
|
||||||
|
"popular": "Popular",
|
||||||
|
"commented": "most Commented"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"copy": "If you already have a human-connection account, login here.",
|
"copy": "If you already have a human-connection account, login here.",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
@ -169,7 +175,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 +295,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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -76,7 +76,7 @@
|
|||||||
"vue-count-to": "~1.0.13",
|
"vue-count-to": "~1.0.13",
|
||||||
"vue-izitoast": "1.1.2",
|
"vue-izitoast": "1.1.2",
|
||||||
"vue-sweetalert-icons": "~3.2.0",
|
"vue-sweetalert-icons": "~3.2.0",
|
||||||
"vuex-i18n": "~1.11.0",
|
"vuex-i18n": "~1.13.0",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -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",
|
||||||
|
|||||||
140
webapp/pages/index.spec.js
Normal file
140
webapp/pages/index.spec.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { config, shallowMount, mount, createLocalVue } from '@vue/test-utils'
|
||||||
|
import PostIndex from './index.vue'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import Styleguide from '@human-connection/styleguide'
|
||||||
|
import Filters from '~/plugins/vue-filters'
|
||||||
|
import VTooltip from 'v-tooltip'
|
||||||
|
import FilterMenu from '~/components/FilterMenu/FilterMenu'
|
||||||
|
|
||||||
|
const localVue = createLocalVue()
|
||||||
|
|
||||||
|
localVue.use(Vuex)
|
||||||
|
localVue.use(Styleguide)
|
||||||
|
localVue.use(Filters)
|
||||||
|
localVue.use(VTooltip)
|
||||||
|
|
||||||
|
config.stubs['no-ssr'] = '<span><slot /></span>'
|
||||||
|
config.stubs['router-link'] = '<span><slot /></span>'
|
||||||
|
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||||
|
|
||||||
|
describe('PostIndex', () => {
|
||||||
|
let wrapper
|
||||||
|
let Wrapper
|
||||||
|
let store
|
||||||
|
let mocks
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new Vuex.Store({
|
||||||
|
getters: {
|
||||||
|
'posts/posts': () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'p23',
|
||||||
|
name: 'It is a post',
|
||||||
|
author: {
|
||||||
|
id: 'u1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'auth/user': () => {
|
||||||
|
return { id: 'u23' }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
mocks = {
|
||||||
|
$t: key => key,
|
||||||
|
$filters: {
|
||||||
|
truncate: a => a,
|
||||||
|
removeLinks: jest.fn(),
|
||||||
|
},
|
||||||
|
// If you are mocking router, than don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html
|
||||||
|
$router: {
|
||||||
|
history: {
|
||||||
|
push: jest.fn(),
|
||||||
|
},
|
||||||
|
push: jest.fn(),
|
||||||
|
},
|
||||||
|
$toast: {
|
||||||
|
success: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
$apollo: {
|
||||||
|
mutate: jest.fn().mockResolvedValue(),
|
||||||
|
queries: {
|
||||||
|
Post: {
|
||||||
|
refetch: jest.fn(),
|
||||||
|
fetchMore: jest.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'p23',
|
||||||
|
name: 'It is a post',
|
||||||
|
author: {
|
||||||
|
id: 'u1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
$route: {
|
||||||
|
query: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shallowMount', () => {
|
||||||
|
Wrapper = () => {
|
||||||
|
return shallowMount(PostIndex, {
|
||||||
|
store,
|
||||||
|
mocks,
|
||||||
|
localVue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('refetches Posts when changeFilterBubble is emitted', () => {
|
||||||
|
wrapper.find(FilterMenu).vm.$emit('changeFilterBubble')
|
||||||
|
expect(mocks.$apollo.queries.Post.refetch).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears the search when the filter menu emits clearSearch', () => {
|
||||||
|
wrapper.find(FilterMenu).vm.$emit('clearSearch')
|
||||||
|
expect(wrapper.vm.hashtag).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the changeFilterBubble if there are hasthags in the route query', () => {
|
||||||
|
mocks.$route.query.hashtag = { id: 'hashtag' }
|
||||||
|
wrapper = Wrapper()
|
||||||
|
expect(mocks.$apollo.queries.Post.refetch).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = mount(PostIndex, {
|
||||||
|
store,
|
||||||
|
mocks,
|
||||||
|
localVue,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the post in the store when there are posts', () => {
|
||||||
|
wrapper
|
||||||
|
.findAll('li')
|
||||||
|
.at(0)
|
||||||
|
.trigger('click')
|
||||||
|
expect(wrapper.vm.sorting).toEqual('createdAt_desc')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads more posts when a user clicks on the load more button', () => {
|
||||||
|
wrapper
|
||||||
|
.findAll('button')
|
||||||
|
.at(2)
|
||||||
|
.trigger('click')
|
||||||
|
expect(mocks.$apollo.queries.Post.fetchMore).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -9,6 +9,17 @@
|
|||||||
@clearSearch="clearSearch"
|
@clearSearch="clearSearch"
|
||||||
/>
|
/>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
|
<ds-flex-item>
|
||||||
|
<div class="sorting-dropdown">
|
||||||
|
<ds-select
|
||||||
|
v-model="selected"
|
||||||
|
:options="sortingOptions"
|
||||||
|
size="large"
|
||||||
|
v-bind:icon-right="sortingIcon"
|
||||||
|
@input="toggleOnlySorting"
|
||||||
|
></ds-select>
|
||||||
|
</div>
|
||||||
|
</ds-flex-item>
|
||||||
<hc-post-card
|
<hc-post-card
|
||||||
v-for="(post, index) in posts"
|
v-for="(post, index) in posts"
|
||||||
:key="post.id"
|
:key="post.id"
|
||||||
@ -53,6 +64,36 @@ export default {
|
|||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
filter: {},
|
filter: {},
|
||||||
hashtag,
|
hashtag,
|
||||||
|
placeholder: this.$t('sorting.newest'),
|
||||||
|
selected: this.$t('sorting.newest'),
|
||||||
|
sortingIcon: 'sort-amount-desc',
|
||||||
|
sorting: 'createdAt_desc',
|
||||||
|
sortingOptions: [
|
||||||
|
{
|
||||||
|
label: this.$t('sorting.newest'),
|
||||||
|
value: 'Newest',
|
||||||
|
icons: 'sort-amount-desc',
|
||||||
|
order: 'createdAt_desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('sorting.oldest'),
|
||||||
|
value: 'Oldest',
|
||||||
|
icons: 'sort-amount-asc',
|
||||||
|
order: 'createdAt_asc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('sorting.popular'),
|
||||||
|
value: 'Popular',
|
||||||
|
icons: 'fire',
|
||||||
|
order: 'shoutedCount_desc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('sorting.commented'),
|
||||||
|
value: 'Commented',
|
||||||
|
icons: 'comment',
|
||||||
|
order: 'commentsCount_desc',
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -89,7 +130,12 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.filter = filter
|
this.filter = filter
|
||||||
this.$apollo.queries.Post.refresh()
|
this.$apollo.queries.Post.refetch()
|
||||||
|
},
|
||||||
|
toggleOnlySorting(x) {
|
||||||
|
this.sortingIcon = x.icons
|
||||||
|
this.sorting = x.order
|
||||||
|
this.$apollo.queries.Post.refetch()
|
||||||
},
|
},
|
||||||
clearSearch() {
|
clearSearch() {
|
||||||
this.$router.push({ path: '/' })
|
this.$router.push({ path: '/' })
|
||||||
@ -144,6 +190,7 @@ export default {
|
|||||||
filter: this.filter,
|
filter: this.filter,
|
||||||
first: this.pageSize,
|
first: this.pageSize,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
orderBy: this.sorting,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
@ -161,4 +208,11 @@ export default {
|
|||||||
transform: translate(-120%, -120%);
|
transform: translate(-120%, -120%);
|
||||||
box-shadow: $box-shadow-x-large;
|
box-shadow: $box-shadow-x-large;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sorting-dropdown {
|
||||||
|
width: 250px;
|
||||||
|
position: relative;
|
||||||
|
float: right;
|
||||||
|
padding: 0 18px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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>
|
||||||
|
<span class="divider">|</span>
|
||||||
<span class="layout-leave-active">|</span>
|
<a name="edit" @click="handleEditSocialMedia(link)">
|
||||||
|
<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>
|
||||||
|
|||||||
@ -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"
|
||||||
@ -11379,10 +11384,10 @@ vue@^2.6.10, vue@^2.6.6:
|
|||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
|
||||||
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==
|
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==
|
||||||
|
|
||||||
vuex-i18n@~1.11.0:
|
vuex-i18n@~1.13.0:
|
||||||
version "1.11.0"
|
version "1.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/vuex-i18n/-/vuex-i18n-1.11.0.tgz#e6cdc95080c445ab2c211cc6b64a907d217639d3"
|
resolved "https://registry.yarnpkg.com/vuex-i18n/-/vuex-i18n-1.13.0.tgz#161c22548e1d3038523f5af49e5730e17a0ca4f2"
|
||||||
integrity sha512-+Eme0C7FS3VFLIWpAwisohC3KcRDw+YcXFANssUZZq16P2C4z8V2VGbEtFHFw0DzkvZcdM2CAkUj6rdMl9wYmg==
|
integrity sha512-r9ZS8NFr8OAIvtlSnqrQLFXxh07QHhkdeGem8RyWFBKej9/yxPAneBEQaLCUM0BOKPpAzBX8t2cjQmtukWbWLg==
|
||||||
|
|
||||||
vuex@^3.1.1:
|
vuex@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user