Merge pull request #919 from Human-Connection/778-categories-for-posts

Post with categories
This commit is contained in:
mattwr18 2019-07-02 12:20:43 -03:00 committed by GitHub
commit 7fa3cc7709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 578 additions and 99 deletions

View File

@ -88,21 +88,26 @@ describe('currentUser { notifications }', () => {
describe('who mentions me again', () => { describe('who mentions me again', () => {
beforeEach(async () => { beforeEach(async () => {
const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>` const updatedContent = `${post.content} One more mention to <a href="/profile/you" class="mention">@al-capone</a>`
const updatedTitle = 'this post has been updated'
// The response `post.content` contains a link but the XSSmiddleware // The response `post.content` contains a link but the XSSmiddleware
// should have the `mention` CSS class removed. I discovered this // should have the `mention` CSS class removed. I discovered this
// during development and thought: A feature not a bug! This way we // during development and thought: A feature not a bug! This way we
// can encode a re-mentioning of users when you edit your post or // can encode a re-mentioning of users when you edit your post or
// comment. // comment.
const createPostMutation = ` const updatePostMutation = `
mutation($id: ID!, $content: String!) { mutation($id: ID!, $title: String!, $content: String!) {
UpdatePost(id: $id, content: $content) { UpdatePost(id: $id, title: $title, content: $content) {
title title
content content
} }
} }
` `
authorClient = new GraphQLClient(host, { headers: authorHeaders }) authorClient = new GraphQLClient(host, { headers: authorHeaders })
await authorClient.request(createPostMutation, { id: post.id, content: updatedContent }) await authorClient.request(updatePostMutation, {
id: post.id,
content: updatedContent,
title: updatedTitle,
})
}) })
it('creates exactly one more notification', async () => { it('creates exactly one more notification', async () => {

View File

@ -105,7 +105,7 @@ const permissions = shield(
Query: { Query: {
'*': deny, '*': deny,
findPosts: allow, findPosts: allow,
Category: isAdmin, Category: allow,
Tag: isAdmin, Tag: isAdmin,
Report: isModerator, Report: isModerator,
Notification: isAdmin, Notification: isAdmin,

View File

@ -17,6 +17,10 @@ export default {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info) return resolve(root, args, context, info)
}, },
UpdatePost: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post')))
return resolve(root, args, context, info)
},
CreateUser: async (resolve, root, args, context, info) => { CreateUser: async (resolve, root, args, context, info) => {
args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User'))) args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User')))
return resolve(root, args, context, info) return resolve(root, args, context, info)

View File

@ -18,9 +18,6 @@ export default {
if (!params.content || content.length < COMMENT_MIN_LENGTH) { if (!params.content || content.length < COMMENT_MIN_LENGTH) {
throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`) throw new UserInputError(`Comment must be at least ${COMMENT_MIN_LENGTH} character long!`)
} }
if (!postId.trim()) {
throw new UserInputError(NO_POST_ERR_MESSAGE)
}
const session = context.driver.session() const session = context.driver.session()
const postQueryRes = await session.run( const postQueryRes = await session.run(

View File

@ -23,7 +23,7 @@ afterEach(async () => {
describe('CreateComment', () => { describe('CreateComment', () => {
const createCommentMutation = gql` const createCommentMutation = gql`
mutation($postId: ID, $content: String!) { mutation($postId: ID!, $content: String!) {
CreateComment(postId: $postId, content: $content) { CreateComment(postId: $postId, content: $content) {
id id
content content
@ -37,13 +37,6 @@ describe('CreateComment', () => {
} }
} }
` `
const commentQueryForPostId = gql`
query($content: String) {
Comment(content: $content) {
postId
}
}
`
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
createCommentVariables = { createCommentVariables = {
@ -191,23 +184,6 @@ describe('CreateComment', () => {
client.request(createCommentMutation, createCommentVariablesWithNonExistentPost), client.request(createCommentMutation, createCommentVariablesWithNonExistentPost),
).rejects.toThrow('Comment cannot be created without a post!') ).rejects.toThrow('Comment cannot be created without a post!')
}) })
it('does not create the comment with the postId as an attribute', async () => {
const commentQueryVariablesByContent = {
content: "I'm authorised to comment",
}
await client.request(createCommentMutation, createCommentVariables)
const { Comment } = await client.request(
commentQueryForPostId,
commentQueryVariablesByContent,
)
expect(Comment).toEqual([
{
postId: null,
},
])
})
}) })
}) })

View File

@ -1,30 +1,74 @@
import { neo4jgraphql } from 'neo4j-graphql-js' import uuid from 'uuid/v4'
import fileUpload from './fileUpload' import fileUpload from './fileUpload'
export default { export default {
Mutation: { Mutation: {
UpdatePost: async (object, params, context, resolveInfo) => { UpdatePost: async (object, params, context, resolveInfo) => {
const { categoryIds } = params
delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
return neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session()
const cypherDeletePreviousRelations = `
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
DELETE previousRelations
RETURN post, category
`
await session.run(cypherDeletePreviousRelations, { params })
let updatePostCypher = `MATCH (post:Post {id: $params.id})
SET post = $params
`
if (categoryIds && categoryIds.length) {
updatePostCypher += `WITH post
UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId})
MERGE (post)-[:CATEGORIZED]->(category)
`
}
updatePostCypher += `RETURN post`
const updatePostVariables = { categoryIds, params }
const transactionRes = await session.run(updatePostCypher, updatePostVariables)
const [post] = transactionRes.records.map(record => {
return record.get('post')
})
session.close()
return post.properties
}, },
CreatePost: async (object, params, context, resolveInfo) => { CreatePost: async (object, params, context, resolveInfo) => {
const { categoryIds } = params
delete params.categoryIds
params = await fileUpload(params, { file: 'imageUpload', url: 'image' }) params = await fileUpload(params, { file: 'imageUpload', url: 'image' })
const result = await neo4jgraphql(object, params, context, resolveInfo, false) params.id = params.id || uuid()
let createPostCypher = `CREATE (post:Post {params})
WITH post
MATCH (author:User {id: $userId})
MERGE (post)<-[:WROTE]-(author)
`
if (categoryIds) {
createPostCypher += `WITH post
UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId})
MERGE (post)-[:CATEGORIZED]->(category)
`
}
createPostCypher += `RETURN post`
const createPostVariables = { userId: context.user.id, categoryIds, params }
const session = context.driver.session() const session = context.driver.session()
await session.run( const transactionRes = await session.run(createPostCypher, createPostVariables)
'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' +
'MERGE (post)<-[:WROTE]-(author) ' + const [post] = transactionRes.records.map(record => {
'RETURN author', return record.get('post')
{ })
userId: context.user.id,
postId: result.id,
},
)
session.close() session.close()
return result return post.properties
}, },
}, },
} }

View File

@ -4,7 +4,34 @@ import { host, login } from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client
const postTitle = 'I am a title'
const postContent = 'Some content'
const oldTitle = 'Old title'
const oldContent = 'Old content'
const newTitle = 'New title'
const newContent = 'New content'
const createPostVariables = { title: postTitle, content: postContent }
const createPostWithCategoriesMutation = `
mutation($title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
id
}
}
`
const creatPostWithCategoriesVariables = {
title: postTitle,
content: postContent,
categoryIds: ['cat9', 'cat4', 'cat15'],
}
const postQueryWithCategories = `
query($id: ID) {
Post(id: $id) {
categories {
id
}
}
}
`
beforeEach(async () => { beforeEach(async () => {
await factory.create('User', { await factory.create('User', {
email: 'test@example.org', email: 'test@example.org',
@ -18,8 +45,8 @@ afterEach(async () => {
describe('CreatePost', () => { describe('CreatePost', () => {
const mutation = ` const mutation = `
mutation { mutation($title: String!, $content: String!) {
CreatePost(title: "I am a title", content: "Some content") { CreatePost(title: $title, content: $content) {
title title
content content
slug slug
@ -32,7 +59,7 @@ describe('CreatePost', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
client = new GraphQLClient(host) client = new GraphQLClient(host)
await expect(client.request(mutation)).rejects.toThrow('Not Authorised') await expect(client.request(mutation, createPostVariables)).rejects.toThrow('Not Authorised')
}) })
}) })
@ -46,15 +73,15 @@ describe('CreatePost', () => {
it('creates a post', async () => { it('creates a post', async () => {
const expected = { const expected = {
CreatePost: { CreatePost: {
title: 'I am a title', title: postTitle,
content: 'Some content', content: postContent,
}, },
} }
await expect(client.request(mutation)).resolves.toMatchObject(expected) await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
}) })
it('assigns the authenticated user as author', async () => { it('assigns the authenticated user as author', async () => {
await client.request(mutation) await client.request(mutation, createPostVariables)
const { User } = await client.request( const { User } = await client.request(
`{ `{
User(email:"test@example.org") { User(email:"test@example.org") {
@ -65,49 +92,75 @@ describe('CreatePost', () => {
}`, }`,
{ headers }, { headers },
) )
expect(User).toEqual([{ contributions: [{ title: 'I am a title' }] }]) expect(User).toEqual([{ contributions: [{ title: postTitle }] }])
}) })
describe('disabled and deleted', () => { describe('disabled and deleted', () => {
it('initially false', async () => { it('initially false', async () => {
const expected = { CreatePost: { disabled: false, deleted: false } } const expected = { CreatePost: { disabled: false, deleted: false } }
await expect(client.request(mutation)).resolves.toMatchObject(expected) await expect(client.request(mutation, createPostVariables)).resolves.toMatchObject(expected)
}) })
}) })
describe('language', () => { describe('language', () => {
it('allows a user to set the language of the post', async () => { it('allows a user to set the language of the post', async () => {
const createPostWithLanguageMutation = ` const createPostWithLanguageMutation = `
mutation { mutation($title: String!, $content: String!, $language: String) {
CreatePost(title: "I am a title", content: "Some content", language: "en") { CreatePost(title: $title, content: $content, language: $language) {
language language
} }
} }
` `
const createPostWithLanguageVariables = {
title: postTitle,
content: postContent,
language: 'en',
}
const expected = { CreatePost: { language: 'en' } } const expected = { CreatePost: { language: 'en' } }
await expect(client.request(createPostWithLanguageMutation)).resolves.toEqual( await expect(
expect.objectContaining(expected), client.request(createPostWithLanguageMutation, createPostWithLanguageVariables),
).resolves.toEqual(expect.objectContaining(expected))
})
})
describe('categories', () => {
it('allows a user to set the categories of the post', async () => {
await Promise.all([
factory.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
}),
factory.create('Category', {
id: 'cat4',
name: 'Environment & Nature',
icon: 'tree',
}),
factory.create('Category', {
id: 'cat15',
name: 'Consumption & Sustainability',
icon: 'shopping-cart',
}),
])
const expected = [{ id: 'cat9' }, { id: 'cat4' }, { id: 'cat15' }]
const postWithCategories = await client.request(
createPostWithCategoriesMutation,
creatPostWithCategoriesVariables,
) )
const postQueryWithCategoriesVariables = {
id: postWithCategories.CreatePost.id,
}
await expect(
client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
}) })
}) })
}) })
}) })
describe('UpdatePost', () => { describe('UpdatePost', () => {
const mutation = ` let updatePostMutation
mutation($id: ID!, $content: String) { let updatePostVariables
UpdatePost(id: $id, content: $content) {
id
content
}
}
`
let variables = {
id: 'p1',
content: 'New content',
}
beforeEach(async () => { beforeEach(async () => {
const asAuthor = Factory() const asAuthor = Factory()
await asAuthor.create('User', { await asAuthor.create('User', {
@ -120,14 +173,32 @@ describe('UpdatePost', () => {
}) })
await asAuthor.create('Post', { await asAuthor.create('Post', {
id: 'p1', id: 'p1',
content: 'Old content', title: oldTitle,
content: oldContent,
}) })
updatePostMutation = `
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]) {
UpdatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
content
}
}
`
updatePostVariables = {
id: 'p1',
title: newTitle,
content: newContent,
categoryIds: null,
}
}) })
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws authorization error', async () => { it('throws authorization error', async () => {
client = new GraphQLClient(host) client = new GraphQLClient(host)
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow(
'Not Authorised',
)
}) })
}) })
@ -139,7 +210,9 @@ describe('UpdatePost', () => {
}) })
it('throws authorization error', async () => { it('throws authorization error', async () => {
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised') await expect(client.request(updatePostMutation, updatePostVariables)).rejects.toThrow(
'Not Authorised',
)
}) })
}) })
@ -151,8 +224,59 @@ describe('UpdatePost', () => {
}) })
it('updates a post', async () => { it('updates a post', async () => {
const expected = { UpdatePost: { id: 'p1', content: 'New content' } } const expected = { UpdatePost: { id: 'p1', content: newContent } }
await expect(client.request(mutation, variables)).resolves.toEqual(expected) await expect(client.request(updatePostMutation, updatePostVariables)).resolves.toEqual(
expected,
)
})
describe('categories', () => {
let postWithCategories
beforeEach(async () => {
await Promise.all([
factory.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
}),
factory.create('Category', {
id: 'cat4',
name: 'Environment & Nature',
icon: 'tree',
}),
factory.create('Category', {
id: 'cat15',
name: 'Consumption & Sustainability',
icon: 'shopping-cart',
}),
factory.create('Category', {
id: 'cat27',
name: 'Animal Protection',
icon: 'paw',
}),
])
postWithCategories = await client.request(
createPostWithCategoriesMutation,
creatPostWithCategoriesVariables,
)
updatePostVariables = {
id: postWithCategories.CreatePost.id,
title: newTitle,
content: newContent,
categoryIds: ['cat27'],
}
})
it('allows a user to update the categories of a post', async () => {
await client.request(updatePostMutation, updatePostVariables)
const expected = [{ id: 'cat27' }]
const postQueryWithCategoriesVariables = {
id: postWithCategories.CreatePost.id,
}
await expect(
client.request(postQueryWithCategories, postQueryWithCategoriesVariables),
).resolves.toEqual({ Post: [{ categories: expect.arrayContaining(expected) }] })
})
}) })
}) })
}) })

View File

@ -1,7 +1,6 @@
type Comment { type Comment {
id: ID! id: ID!
activityId: String activityId: String
postId: ID
author: User @relation(name: "WROTE", direction: "IN") author: User @relation(name: "WROTE", direction: "IN")
content: String! content: String!
contentExcerpt: String contentExcerpt: String
@ -11,4 +10,24 @@ type Comment {
deleted: Boolean deleted: Boolean
disabled: Boolean disabled: Boolean
disabledBy: User @relation(name: "DISABLED", direction: "IN") disabledBy: User @relation(name: "DISABLED", direction: "IN")
} }
type Mutation {
CreateComment(
id: ID
postId: ID!
content: String!
contentExcerpt: String
deleted: Boolean
disabled: Boolean
createdAt: String
): Comment
UpdateComment(
id: ID!
content: String
contentExcerpt: String
deleted: Boolean
disabled: Boolean
): Comment
DeleteComment(id: ID!): Comment
}

View File

@ -49,3 +49,42 @@ type Post {
""" """
) )
} }
type Mutation {
CreatePost(
id: ID
activityId: String
objectId: String
title: String!
slug: String
content: String!
image: String
imageUpload: Upload
visibility: Visibility
deleted: Boolean
disabled: Boolean
createdAt: String
updatedAt: String
language: String
categoryIds: [ID]
contentExcerpt: String
): Post
UpdatePost(
id: ID!
activityId: String
objectId: String
title: String!
slug: String
content: String!
contentExcerpt: String
image: String
imageUpload: Upload
visibility: Visibility
deleted: Boolean
disabled: Boolean
createdAt: String
updatedAt: String
language: String
categoryIds: [ID]
): Post
}

View File

@ -10,7 +10,7 @@ export default function(params) {
return { return {
mutation: ` mutation: `
mutation($id: ID!, $postId: ID, $content: String!) { mutation($id: ID!, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) { CreateComment(id: $id, postId: $postId, content: $content) {
id id
} }

View File

@ -0,0 +1,108 @@
import { mount, createLocalVue } from '@vue/test-utils'
import CategoriesSelect from './CategoriesSelect'
import Styleguide from '@human-connection/styleguide'
const localVue = createLocalVue()
localVue.use(Styleguide)
describe('CategoriesSelect.vue', () => {
let wrapper
let mocks
let democracyAndPolitics
let environmentAndNature
let consumptionAndSustainablity
const categories = [
{
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
},
{
id: 'cat4',
name: 'Environment & Nature',
icon: 'tree',
},
{
id: 'cat15',
name: 'Consumption & Sustainability',
icon: 'shopping-cart',
},
{
name: 'Cooperation & Development',
icon: 'users',
id: 'cat8',
},
]
beforeEach(() => {
mocks = {
$t: jest.fn(),
}
})
describe('shallowMount', () => {
const Wrapper = () => {
return mount(CategoriesSelect, { mocks, localVue })
}
beforeEach(() => {
wrapper = Wrapper()
})
describe('toggleCategory', () => {
beforeEach(() => {
wrapper.vm.categories = categories
democracyAndPolitics = wrapper.findAll('button').at(0)
democracyAndPolitics.trigger('click')
})
it('adds categories to selectedCategoryIds when clicked', () => {
expect(wrapper.vm.selectedCategoryIds).toEqual([categories[0].id])
})
it('emits an updateCategories event when the selectedCategoryIds changes', () => {
expect(wrapper.emitted().updateCategories[0][0]).toEqual([categories[0].id])
})
it('removes categories when clicked a second time', () => {
democracyAndPolitics.trigger('click')
expect(wrapper.vm.selectedCategoryIds).toEqual([])
})
it('changes the selectedCount when selectedCategoryIds is updated', () => {
expect(wrapper.vm.selectedCount).toEqual(1)
democracyAndPolitics.trigger('click')
expect(wrapper.vm.selectedCount).toEqual(0)
})
it('sets a category to active when it has been selected', () => {
expect(wrapper.vm.isActive(categories[0].id)).toEqual(true)
})
describe('maximum', () => {
beforeEach(() => {
environmentAndNature = wrapper.findAll('button').at(1)
consumptionAndSustainablity = wrapper.findAll('button').at(2)
environmentAndNature.trigger('click')
consumptionAndSustainablity.trigger('click')
})
it('allows three categories to be selected', () => {
expect(wrapper.vm.selectedCategoryIds).toEqual([
categories[0].id,
categories[1].id,
categories[2].id,
])
})
it('sets reachedMaximum to true after three', () => {
expect(wrapper.vm.reachedMaximum).toEqual(true)
})
it('sets other categories to disabled after three', () => {
expect(wrapper.vm.isDisabled(categories[3].id)).toEqual(true)
})
})
})
})
})

View File

@ -0,0 +1,102 @@
<template>
<div>
<ds-flex :gutter="{ base: 'xx-small', md: 'small', lg: 'xx-small' }">
<div v-for="category in categories" :key="category.id">
<ds-flex-item>
<ds-button
size="small"
@click.prevent="toggleCategory(category.id)"
:primary="isActive(category.id)"
:disabled="isDisabled(category.id)"
>
<ds-icon :name="category.icon" />
{{ category.name }}
</ds-button>
</ds-flex-item>
</div>
</ds-flex>
<p class="small-info">
{{
$t('contribution.categories.infoSelectedNoOfMaxCategories', {
chosen: selectedCount,
max: selectedMax,
})
}}
</p>
</div>
</template>
<script>
import gql from 'graphql-tag'
export default {
props: {
existingCategoryIds: { type: Array, default: () => [] },
},
data() {
return {
categories: null,
selectedMax: 3,
selectedCategoryIds: [],
}
},
computed: {
selectedCount() {
return this.selectedCategoryIds.length
},
reachedMaximum() {
return this.selectedCount >= this.selectedMax
},
},
watch: {
selectedCategoryIds(categoryIds) {
this.$emit('updateCategories', categoryIds)
},
existingCategoryIds: {
immediate: true,
handler: function(existingCategoryIds) {
if (!existingCategoryIds || !existingCategoryIds.length) {
return
}
this.selectedCategoryIds = existingCategoryIds
},
},
},
methods: {
toggleCategory(id) {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
this.selectedCategoryIds.splice(index, 1)
} else {
this.selectedCategoryIds.push(id)
}
},
isActive(id) {
const index = this.selectedCategoryIds.indexOf(id)
if (index > -1) {
return true
}
return false
},
isDisabled(id) {
return !!(this.reachedMaximum && !this.isActive(id))
},
},
apollo: {
Category: {
query() {
return gql(`{
Category {
id
name
icon
}
}`)
},
result(result) {
this.categories = result.data.Category
},
},
},
}
</script>

View File

@ -1,8 +1,9 @@
import { config, mount, createLocalVue } from '@vue/test-utils' import { config, mount, createLocalVue } from '@vue/test-utils'
import ContributionForm from './index.vue' import ContributionForm from './ContributionForm.vue'
import Styleguide from '@human-connection/styleguide' import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex' import Vuex from 'vuex'
import PostMutations from '~/graphql/PostMutations.js' import PostMutations from '~/graphql/PostMutations.js'
import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
import Filters from '~/plugins/vue-filters' import Filters from '~/plugins/vue-filters'
import TeaserImage from '~/components/TeaserImage/TeaserImage' import TeaserImage from '~/components/TeaserImage/TeaserImage'
@ -28,7 +29,7 @@ describe('ContributionForm.vue', () => {
file: { filename: 'avataar.svg', previewElement: '' }, file: { filename: 'avataar.svg', previewElement: '' },
url: 'someUrlToImage', url: 'someUrlToImage',
} }
const image = '/uploads/1562010976466-avataaars'
beforeEach(() => { beforeEach(() => {
mocks = { mocks = {
$t: jest.fn(), $t: jest.fn(),
@ -112,7 +113,9 @@ describe('ContributionForm.vue', () => {
content: postContent, content: postContent,
language: 'en', language: 'en',
id: null, id: null,
categoryIds: null,
imageUpload: null, imageUpload: null,
image: null,
}, },
} }
postTitleInput = wrapper.find('.ds-input') postTitleInput = wrapper.find('.ds-input')
@ -137,6 +140,14 @@ describe('ContributionForm.vue', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
it('supports adding categories', async () => {
const categoryIds = ['cat12', 'cat15', 'cat37']
expectedParams.variables.categoryIds = categoryIds
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
await wrapper.find('form').trigger('submit')
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)
@ -189,7 +200,8 @@ describe('ContributionForm.vue', () => {
title: 'dies ist ein Post', title: 'dies ist ein Post',
content: 'auf Deutsch geschrieben', content: 'auf Deutsch geschrieben',
language: 'de', language: 'de',
imageUpload, image,
categories: [{ id: 'cat12', name: 'Democracy & Politics' }],
}, },
} }
wrapper = Wrapper() wrapper = Wrapper()
@ -219,7 +231,9 @@ describe('ContributionForm.vue', () => {
content: postContent, content: postContent,
language: propsData.contribution.language, language: propsData.contribution.language,
id: propsData.contribution.id, id: propsData.contribution.id,
imageUpload, categoryIds: ['cat12'],
image,
imageUpload: null,
}, },
} }
postTitleInput = wrapper.find('.ds-input') postTitleInput = wrapper.find('.ds-input')
@ -228,6 +242,17 @@ describe('ContributionForm.vue', () => {
await wrapper.find('form').trigger('submit') await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
}) })
it('supports updating categories', async () => {
const categoryIds = ['cat3', 'cat51', 'cat37']
postTitleInput = wrapper.find('.ds-input')
postTitleInput.setValue(postTitle)
wrapper.vm.updateEditorContent(postContent)
expectedParams.variables.categoryIds = categoryIds
wrapper.find(CategoriesSelect).vm.$emit('updateCategories', categoryIds)
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
})
}) })
}) })
}) })

View File

@ -14,6 +14,11 @@
<hc-editor :users="users" :value="form.content" @input="updateEditorContent" /> <hc-editor :users="users" :value="form.content" @input="updateEditorContent" />
</no-ssr> </no-ssr>
<ds-space margin-bottom="xxx-large" /> <ds-space margin-bottom="xxx-large" />
<hc-categories-select
model="categoryIds"
@updateCategories="updateCategories"
:existingCategoryIds="form.categoryIds"
/>
<ds-flex class="contribution-form-footer"> <ds-flex class="contribution-form-footer">
<ds-flex-item :width="{ base: '10%', sm: '10%', md: '10%', lg: '15%' }" /> <ds-flex-item :width="{ base: '10%', sm: '10%', md: '10%', lg: '15%' }" />
<ds-flex-item :width="{ base: '80%', sm: '30%', md: '30%', lg: '20%' }"> <ds-flex-item :width="{ base: '80%', sm: '30%', md: '30%', lg: '20%' }">
@ -32,7 +37,7 @@
:disabled="loading || disabled" :disabled="loading || disabled"
ghost ghost
class="cancel-button" class="cancel-button"
@click="$router.back()" @click.prevent="$router.back()"
> >
{{ $t('actions.cancel') }} {{ $t('actions.cancel') }}
</ds-button> </ds-button>
@ -58,11 +63,13 @@ import HcEditor from '~/components/Editor'
import orderBy from 'lodash/orderBy' import orderBy from 'lodash/orderBy'
import locales from '~/locales' import locales from '~/locales'
import PostMutations from '~/graphql/PostMutations.js' import PostMutations from '~/graphql/PostMutations.js'
import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect'
import HcTeaserImage from '~/components/TeaserImage/TeaserImage' import HcTeaserImage from '~/components/TeaserImage/TeaserImage'
export default { export default {
components: { components: {
HcEditor, HcEditor,
HcCategoriesSelect,
HcTeaserImage, HcTeaserImage,
}, },
props: { props: {
@ -74,8 +81,10 @@ export default {
title: '', title: '',
content: '', content: '',
teaserImage: null, teaserImage: null,
image: null,
language: null, language: null,
languageOptions: [], languageOptions: [],
categoryIds: null,
}, },
formSchema: { formSchema: {
title: { required: true, min: 3, max: 64 }, title: { required: true, min: 3, max: 64 },
@ -99,7 +108,8 @@ export default {
this.slug = contribution.slug this.slug = contribution.slug
this.form.content = contribution.content this.form.content = contribution.content
this.form.title = contribution.title this.form.title = contribution.title
this.form.teaserImage = contribution.imageUpload this.form.image = contribution.image
this.form.categoryIds = this.categoryIds(contribution.categories)
}, },
}, },
}, },
@ -117,7 +127,7 @@ export default {
}, },
methods: { methods: {
submit() { submit() {
const { title, content, teaserImage } = this.form const { title, content, image, teaserImage, categoryIds } = this.form
let language let language
if (this.form.language) { if (this.form.language) {
language = this.form.language.value language = this.form.language.value
@ -134,7 +144,9 @@ export default {
id: this.id, id: this.id,
title, title,
content, content,
categoryIds,
language, language,
image,
imageUpload: teaserImage, imageUpload: teaserImage,
}, },
}) })
@ -142,7 +154,6 @@ export default {
this.loading = false this.loading = false
this.$toast.success(this.$t('contribution.success')) this.$toast.success(this.$t('contribution.success'))
this.disabled = true this.disabled = true
const result = res.data[this.id ? 'UpdatePost' : 'CreatePost'] const result = res.data[this.id ? 'UpdatePost' : 'CreatePost']
this.$router.push({ this.$router.push({
@ -165,9 +176,19 @@ export default {
this.form.languageOptions.push({ label: locale.name, value: locale.code }) this.form.languageOptions.push({ label: locale.name, value: locale.code })
}) })
}, },
updateCategories(ids) {
this.form.categoryIds = ids
},
addTeaserImage(file) { addTeaserImage(file) {
this.form.teaserImage = file this.form.teaserImage = file
}, },
categoryIds(categories) {
let categoryIds = []
categories.map(categoryId => {
categoryIds.push(categoryId.id)
})
return categoryIds
},
}, },
apollo: { apollo: {
User: { User: {

View File

@ -3,7 +3,7 @@ import gql from 'graphql-tag'
export default () => { export default () => {
return { return {
CreateComment: gql` CreateComment: gql`
mutation($postId: ID, $content: String!) { mutation($postId: ID!, $content: String!) {
CreateComment(postId: $postId, content: $content) { CreateComment(postId: $postId, content: $content) {
id id
contentExcerpt contentExcerpt

View File

@ -3,20 +3,25 @@ import gql from 'graphql-tag'
export default () => { export default () => {
return { return {
CreatePost: gql` CreatePost: gql`
mutation($title: String!, $content: String!, $language: String, $imageUpload: Upload) { mutation(
$title: String!
$content: String!
$language: String
$categoryIds: [ID]
$imageUpload: Upload
) {
CreatePost( CreatePost(
title: $title title: $title
content: $content content: $content
language: $language language: $language
categoryIds: $categoryIds
imageUpload: $imageUpload imageUpload: $imageUpload
) { ) {
id
title title
slug slug
content content
contentExcerpt contentExcerpt
language language
imageUpload
} }
} }
`, `,
@ -27,6 +32,8 @@ export default () => {
$content: String! $content: String!
$language: String $language: String
$imageUpload: Upload $imageUpload: Upload
$categoryIds: [ID]
$image: String
) { ) {
UpdatePost( UpdatePost(
id: $id id: $id
@ -34,6 +41,8 @@ export default () => {
content: $content content: $content
language: $language language: $language
imageUpload: $imageUpload imageUpload: $imageUpload
categoryIds: $categoryIds
image: $image
) { ) {
id id
title title
@ -41,7 +50,7 @@ export default () => {
content content
contentExcerpt contentExcerpt
language language
imageUpload image
} }
} }
`, `,

View File

@ -332,7 +332,10 @@
"filterFollow": "Beiträge filtern von Usern denen ich folge", "filterFollow": "Beiträge filtern von Usern denen ich folge",
"filterALL": "Alle Beiträge anzeigen", "filterALL": "Alle Beiträge anzeigen",
"success": "Gespeichert!", "success": "Gespeichert!",
"languageSelectLabel": "Sprache" "languageSelectLabel": "Sprache",
"categories": {
"infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt"
}
} }
} }

View File

@ -331,6 +331,9 @@
"filterFollow": "Filter contributions from users I follow", "filterFollow": "Filter contributions from users I follow",
"filterALL": "View all contributions", "filterALL": "View all contributions",
"success": "Saved!", "success": "Saved!",
"languageSelectLabel": "Language" "languageSelectLabel": "Language",
"categories": {
"infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected"
}
} }
} }

View File

@ -8,7 +8,7 @@
</template> </template>
<script> <script>
import HcContributionForm from '~/components/ContributionForm' import HcContributionForm from '~/components/ContributionForm/ContributionForm'
export default { export default {
components: { components: {

View File

@ -9,7 +9,7 @@
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import HcContributionForm from '~/components/ContributionForm' import HcContributionForm from '~/components/ContributionForm/ContributionForm'
export default { export default {
components: { components: {