diff --git a/backend/src/middleware/notifications/spec.js b/backend/src/middleware/notifications/spec.js index 985654b0f..d214a5571 100644 --- a/backend/src/middleware/notifications/spec.js +++ b/backend/src/middleware/notifications/spec.js @@ -88,21 +88,26 @@ describe('currentUser { notifications }', () => { describe('who mentions me again', () => { beforeEach(async () => { const updatedContent = `${post.content} One more mention to @al-capone` + const updatedTitle = 'this post has been updated' // The response `post.content` contains a link but the XSSmiddleware // should have the `mention` CSS class removed. I discovered this // 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 // comment. - const createPostMutation = ` - mutation($id: ID!, $content: String!) { - UpdatePost(id: $id, content: $content) { + const updatePostMutation = ` + mutation($id: ID!, $title: String!, $content: String!) { + UpdatePost(id: $id, title: $title, content: $content) { title content } } ` 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 () => { diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index dbcde849c..66c61c12e 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -105,7 +105,7 @@ const permissions = shield( Query: { '*': deny, findPosts: allow, - Category: isAdmin, + Category: allow, Tag: isAdmin, Report: isModerator, Notification: isAdmin, diff --git a/backend/src/middleware/sluggifyMiddleware.js b/backend/src/middleware/sluggifyMiddleware.js index 2b1f25d5c..226bef8e5 100644 --- a/backend/src/middleware/sluggifyMiddleware.js +++ b/backend/src/middleware/sluggifyMiddleware.js @@ -17,6 +17,10 @@ export default { args.slug = args.slug || (await uniqueSlug(args.title, isUniqueFor(context, 'Post'))) 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) => { args.slug = args.slug || (await uniqueSlug(args.name, isUniqueFor(context, 'User'))) return resolve(root, args, context, info) diff --git a/backend/src/schema/resolvers/comments.js b/backend/src/schema/resolvers/comments.js index d2e296596..7aef63c59 100644 --- a/backend/src/schema/resolvers/comments.js +++ b/backend/src/schema/resolvers/comments.js @@ -18,9 +18,6 @@ export default { if (!params.content || content.length < COMMENT_MIN_LENGTH) { 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 postQueryRes = await session.run( diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index 55b946bb9..07462ed49 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -23,7 +23,7 @@ afterEach(async () => { describe('CreateComment', () => { const createCommentMutation = gql` - mutation($postId: ID, $content: String!) { + mutation($postId: ID!, $content: String!) { CreateComment(postId: $postId, content: $content) { id content @@ -37,13 +37,6 @@ describe('CreateComment', () => { } } ` - const commentQueryForPostId = gql` - query($content: String) { - Comment(content: $content) { - postId - } - } - ` describe('unauthenticated', () => { it('throws authorization error', async () => { createCommentVariables = { @@ -191,23 +184,6 @@ describe('CreateComment', () => { client.request(createCommentMutation, createCommentVariablesWithNonExistentPost), ).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, - }, - ]) - }) }) }) diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index ea962a662..0c8dfb7f0 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -1,30 +1,74 @@ -import { neo4jgraphql } from 'neo4j-graphql-js' +import uuid from 'uuid/v4' import fileUpload from './fileUpload' export default { Mutation: { UpdatePost: async (object, params, context, resolveInfo) => { + const { categoryIds } = params + delete params.categoryIds 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) => { + const { categoryIds } = params + delete params.categoryIds 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() - await session.run( - 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + - 'MERGE (post)<-[:WROTE]-(author) ' + - 'RETURN author', - { - userId: context.user.id, - postId: result.id, - }, - ) + const transactionRes = await session.run(createPostCypher, createPostVariables) + + const [post] = transactionRes.records.map(record => { + return record.get('post') + }) + session.close() - return result + return post.properties }, }, } diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 3bff53ddb..763945527 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -4,7 +4,34 @@ import { host, login } from '../../jest/helpers' const factory = Factory() 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 () => { await factory.create('User', { email: 'test@example.org', @@ -18,8 +45,8 @@ afterEach(async () => { describe('CreatePost', () => { const mutation = ` - mutation { - CreatePost(title: "I am a title", content: "Some content") { + mutation($title: String!, $content: String!) { + CreatePost(title: $title, content: $content) { title content slug @@ -32,7 +59,7 @@ describe('CreatePost', () => { describe('unauthenticated', () => { it('throws authorization error', async () => { 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 () => { const expected = { CreatePost: { - title: 'I am a title', - content: 'Some content', + title: postTitle, + 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 () => { - await client.request(mutation) + await client.request(mutation, createPostVariables) const { User } = await client.request( `{ User(email:"test@example.org") { @@ -65,49 +92,75 @@ describe('CreatePost', () => { }`, { headers }, ) - expect(User).toEqual([{ contributions: [{ title: 'I am a title' }] }]) + expect(User).toEqual([{ contributions: [{ title: postTitle }] }]) }) describe('disabled and deleted', () => { it('initially false', async () => { 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', () => { it('allows a user to set the language of the post', async () => { const createPostWithLanguageMutation = ` - mutation { - CreatePost(title: "I am a title", content: "Some content", language: "en") { + mutation($title: String!, $content: String!, $language: String) { + CreatePost(title: $title, content: $content, language: $language) { language } } ` + const createPostWithLanguageVariables = { + title: postTitle, + content: postContent, + language: 'en', + } const expected = { CreatePost: { language: 'en' } } - await expect(client.request(createPostWithLanguageMutation)).resolves.toEqual( - expect.objectContaining(expected), + await expect( + 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', () => { - const mutation = ` - mutation($id: ID!, $content: String) { - UpdatePost(id: $id, content: $content) { - id - content - } - } - ` - - let variables = { - id: 'p1', - content: 'New content', - } - + let updatePostMutation + let updatePostVariables beforeEach(async () => { const asAuthor = Factory() await asAuthor.create('User', { @@ -120,14 +173,32 @@ describe('UpdatePost', () => { }) await asAuthor.create('Post', { 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', () => { it('throws authorization error', async () => { 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 () => { - 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 () => { - const expected = { UpdatePost: { id: 'p1', content: 'New content' } } - await expect(client.request(mutation, variables)).resolves.toEqual(expected) + const expected = { UpdatePost: { id: 'p1', content: newContent } } + 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) }] }) + }) }) }) }) diff --git a/backend/src/schema/types/type/Comment.gql b/backend/src/schema/types/type/Comment.gql index 077366e8a..441fba179 100644 --- a/backend/src/schema/types/type/Comment.gql +++ b/backend/src/schema/types/type/Comment.gql @@ -1,7 +1,6 @@ type Comment { id: ID! activityId: String - postId: ID author: User @relation(name: "WROTE", direction: "IN") content: String! contentExcerpt: String @@ -11,4 +10,24 @@ type Comment { deleted: Boolean disabled: Boolean disabledBy: User @relation(name: "DISABLED", direction: "IN") -} \ No newline at end of file +} + +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 +} diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 271d92750..deb1d8f85 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -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 +} diff --git a/backend/src/seed/factories/comments.js b/backend/src/seed/factories/comments.js index b1079e392..20933e947 100644 --- a/backend/src/seed/factories/comments.js +++ b/backend/src/seed/factories/comments.js @@ -10,7 +10,7 @@ export default function(params) { return { mutation: ` - mutation($id: ID!, $postId: ID, $content: String!) { + mutation($id: ID!, $postId: ID!, $content: String!) { CreateComment(id: $id, postId: $postId, content: $content) { id } diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.spec.js b/webapp/components/CategoriesSelect/CategoriesSelect.spec.js new file mode 100644 index 000000000..199dacb74 --- /dev/null +++ b/webapp/components/CategoriesSelect/CategoriesSelect.spec.js @@ -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) + }) + }) + }) + }) +}) diff --git a/webapp/components/CategoriesSelect/CategoriesSelect.vue b/webapp/components/CategoriesSelect/CategoriesSelect.vue new file mode 100644 index 000000000..163f31419 --- /dev/null +++ b/webapp/components/CategoriesSelect/CategoriesSelect.vue @@ -0,0 +1,102 @@ + + + diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index caeeafdf6..0813d16f0 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -1,8 +1,9 @@ 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 Vuex from 'vuex' import PostMutations from '~/graphql/PostMutations.js' +import CategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect' import Filters from '~/plugins/vue-filters' import TeaserImage from '~/components/TeaserImage/TeaserImage' @@ -28,7 +29,7 @@ describe('ContributionForm.vue', () => { file: { filename: 'avataar.svg', previewElement: '' }, url: 'someUrlToImage', } - + const image = '/uploads/1562010976466-avataaars' beforeEach(() => { mocks = { $t: jest.fn(), @@ -112,7 +113,9 @@ describe('ContributionForm.vue', () => { content: postContent, language: 'en', id: null, + categoryIds: null, imageUpload: null, + image: null, }, } postTitleInput = wrapper.find('.ds-input') @@ -137,6 +140,14 @@ describe('ContributionForm.vue', () => { 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 () => { expectedParams.variables.imageUpload = imageUpload wrapper.find(TeaserImage).vm.$emit('addTeaserImage', imageUpload) @@ -189,7 +200,8 @@ describe('ContributionForm.vue', () => { title: 'dies ist ein Post', content: 'auf Deutsch geschrieben', language: 'de', - imageUpload, + image, + categories: [{ id: 'cat12', name: 'Democracy & Politics' }], }, } wrapper = Wrapper() @@ -219,7 +231,9 @@ describe('ContributionForm.vue', () => { content: postContent, language: propsData.contribution.language, id: propsData.contribution.id, - imageUpload, + categoryIds: ['cat12'], + image, + imageUpload: null, }, } postTitleInput = wrapper.find('.ds-input') @@ -228,6 +242,17 @@ describe('ContributionForm.vue', () => { await wrapper.find('form').trigger('submit') 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)) + }) }) }) }) diff --git a/webapp/components/ContributionForm/index.vue b/webapp/components/ContributionForm/ContributionForm.vue similarity index 86% rename from webapp/components/ContributionForm/index.vue rename to webapp/components/ContributionForm/ContributionForm.vue index 57eb105be..c6bb2cdc4 100644 --- a/webapp/components/ContributionForm/index.vue +++ b/webapp/components/ContributionForm/ContributionForm.vue @@ -14,6 +14,11 @@ + @@ -32,7 +37,7 @@ :disabled="loading || disabled" ghost class="cancel-button" - @click="$router.back()" + @click.prevent="$router.back()" > {{ $t('actions.cancel') }} @@ -58,11 +63,13 @@ import HcEditor from '~/components/Editor' import orderBy from 'lodash/orderBy' import locales from '~/locales' import PostMutations from '~/graphql/PostMutations.js' +import HcCategoriesSelect from '~/components/CategoriesSelect/CategoriesSelect' import HcTeaserImage from '~/components/TeaserImage/TeaserImage' export default { components: { HcEditor, + HcCategoriesSelect, HcTeaserImage, }, props: { @@ -74,8 +81,10 @@ export default { title: '', content: '', teaserImage: null, + image: null, language: null, languageOptions: [], + categoryIds: null, }, formSchema: { title: { required: true, min: 3, max: 64 }, @@ -99,7 +108,8 @@ export default { this.slug = contribution.slug this.form.content = contribution.content 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: { submit() { - const { title, content, teaserImage } = this.form + const { title, content, image, teaserImage, categoryIds } = this.form let language if (this.form.language) { language = this.form.language.value @@ -134,7 +144,9 @@ export default { id: this.id, title, content, + categoryIds, language, + image, imageUpload: teaserImage, }, }) @@ -142,7 +154,6 @@ export default { this.loading = false this.$toast.success(this.$t('contribution.success')) this.disabled = true - const result = res.data[this.id ? 'UpdatePost' : 'CreatePost'] this.$router.push({ @@ -165,9 +176,19 @@ export default { this.form.languageOptions.push({ label: locale.name, value: locale.code }) }) }, + updateCategories(ids) { + this.form.categoryIds = ids + }, addTeaserImage(file) { this.form.teaserImage = file }, + categoryIds(categories) { + let categoryIds = [] + categories.map(categoryId => { + categoryIds.push(categoryId.id) + }) + return categoryIds + }, }, apollo: { User: { diff --git a/webapp/graphql/CommentMutations.js b/webapp/graphql/CommentMutations.js index efaf6cd49..f94ea7ac9 100644 --- a/webapp/graphql/CommentMutations.js +++ b/webapp/graphql/CommentMutations.js @@ -3,7 +3,7 @@ import gql from 'graphql-tag' export default () => { return { CreateComment: gql` - mutation($postId: ID, $content: String!) { + mutation($postId: ID!, $content: String!) { CreateComment(postId: $postId, content: $content) { id contentExcerpt diff --git a/webapp/graphql/PostMutations.js b/webapp/graphql/PostMutations.js index 31e618bfb..dba8d32a5 100644 --- a/webapp/graphql/PostMutations.js +++ b/webapp/graphql/PostMutations.js @@ -3,20 +3,25 @@ import gql from 'graphql-tag' export default () => { return { CreatePost: gql` - mutation($title: String!, $content: String!, $language: String, $imageUpload: Upload) { + mutation( + $title: String! + $content: String! + $language: String + $categoryIds: [ID] + $imageUpload: Upload + ) { CreatePost( title: $title content: $content language: $language + categoryIds: $categoryIds imageUpload: $imageUpload ) { - id title slug content contentExcerpt language - imageUpload } } `, @@ -27,6 +32,8 @@ export default () => { $content: String! $language: String $imageUpload: Upload + $categoryIds: [ID] + $image: String ) { UpdatePost( id: $id @@ -34,6 +41,8 @@ export default () => { content: $content language: $language imageUpload: $imageUpload + categoryIds: $categoryIds + image: $image ) { id title @@ -41,7 +50,7 @@ export default () => { content contentExcerpt language - imageUpload + image } } `, diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 041539acd..bd4ca60ca 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -332,7 +332,10 @@ "filterFollow": "Beiträge filtern von Usern denen ich folge", "filterALL": "Alle Beiträge anzeigen", "success": "Gespeichert!", - "languageSelectLabel": "Sprache" + "languageSelectLabel": "Sprache", + "categories": { + "infoSelectedNoOfMaxCategories": "{chosen} von {max} Kategorien ausgewählt" + } } } diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 2442dc202..8f09c5fe1 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -331,6 +331,9 @@ "filterFollow": "Filter contributions from users I follow", "filterALL": "View all contributions", "success": "Saved!", - "languageSelectLabel": "Language" + "languageSelectLabel": "Language", + "categories": { + "infoSelectedNoOfMaxCategories": "{chosen} of {max} categories selected" + } } } diff --git a/webapp/pages/post/create.vue b/webapp/pages/post/create.vue index a5781032b..c6b76287f 100644 --- a/webapp/pages/post/create.vue +++ b/webapp/pages/post/create.vue @@ -8,7 +8,7 @@