mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge branch '260-add-comment-form' of https://github.com/Human-Connection/Human-Connection into 260-add-comment-form
This commit is contained in:
commit
3fa064986e
@ -11,6 +11,7 @@ import shout from './resolvers/shout.js'
|
||||
import rewards from './resolvers/rewards.js'
|
||||
import socialMedia from './resolvers/socialMedia.js'
|
||||
import notifications from './resolvers/notifications'
|
||||
import comments from './resolvers/comments'
|
||||
|
||||
export const typeDefs = fs
|
||||
.readFileSync(
|
||||
@ -22,7 +23,8 @@ export const resolvers = {
|
||||
Query: {
|
||||
...statistics.Query,
|
||||
...userManagement.Query,
|
||||
...notifications.Query
|
||||
...notifications.Query,
|
||||
...comments.Query
|
||||
},
|
||||
Mutation: {
|
||||
...userManagement.Mutation,
|
||||
@ -33,6 +35,7 @@ export const resolvers = {
|
||||
...shout.Mutation,
|
||||
...rewards.Mutation,
|
||||
...socialMedia.Mutation,
|
||||
...notifications.Mutation
|
||||
...notifications.Mutation,
|
||||
...comments.Mutation
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +86,8 @@ const permissions = shield({
|
||||
unshout: isAuthenticated,
|
||||
changePassword: isAuthenticated,
|
||||
enable: isModerator,
|
||||
disable: isModerator
|
||||
disable: isModerator,
|
||||
CreateComment: isAuthenticated
|
||||
// CreateUser: allow,
|
||||
},
|
||||
User: {
|
||||
|
||||
@ -23,21 +23,19 @@ beforeAll(async () => {
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
factory.create('Comment', { id: 'c2', content: 'Enabled comment on public post' })
|
||||
factory.create('Comment', { id: 'c2', postId: 'p3', content: 'Enabled comment on public post' })
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' }),
|
||||
factory.relate('Comment', 'Post', { from: 'c2', to: 'p3' })
|
||||
factory.relate('Comment', 'Author', { from: 'u1', to: 'c2' })
|
||||
])
|
||||
|
||||
const asTroll = Factory()
|
||||
await asTroll.authenticateAs({ email: 'troll@example.org', password: '1234' })
|
||||
await asTroll.create('Post', { id: 'p2', title: 'Disabled post', content: 'This is an offensive post content', image: '/some/offensive/image.jpg', deleted: false })
|
||||
await asTroll.create('Comment', { id: 'c1', content: 'Disabled comment' })
|
||||
await asTroll.create('Comment', { id: 'c1', postId: 'p3', content: 'Disabled comment' })
|
||||
await Promise.all([
|
||||
asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' }),
|
||||
asTroll.relate('Comment', 'Post', { from: 'c1', to: 'p3' })
|
||||
asTroll.relate('Comment', 'Author', { from: 'u2', to: 'c1' })
|
||||
])
|
||||
|
||||
const asModerator = Factory()
|
||||
|
||||
45
backend/src/resolvers/comments.js
Normal file
45
backend/src/resolvers/comments.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
CommentByPost: async (object, params, context, resolveInfo) => {
|
||||
const { postId } = params
|
||||
|
||||
const session = context.driver.session()
|
||||
const transactionRes = await session.run(`
|
||||
MATCH (comment:Comment)-[:COMMENTS]->(post:Post {id: $postId})
|
||||
RETURN comment {.id, .contentExcerpt, .createdAt}`, {
|
||||
postId
|
||||
})
|
||||
|
||||
session.close()
|
||||
let comments = []
|
||||
transactionRes.records.map(record => {
|
||||
comments.push(record.get('comment'))
|
||||
})
|
||||
|
||||
return comments
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
CreateComment: async (object, params, context, resolveInfo) => {
|
||||
const { postId } = params
|
||||
delete params.postId
|
||||
const comment = await neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
|
||||
const session = context.driver.session()
|
||||
|
||||
await session.run(`
|
||||
MATCH (post:Post {id: $postId}), (comment:Comment {id: $commentId})
|
||||
MERGE (post)<-[:COMMENTS]-(comment)
|
||||
RETURN comment {.id, .content}`, {
|
||||
postId,
|
||||
commentId: comment.id
|
||||
}
|
||||
)
|
||||
session.close()
|
||||
|
||||
return comment
|
||||
}
|
||||
}
|
||||
}
|
||||
61
backend/src/resolvers/comments.spec.js
Normal file
61
backend/src/resolvers/comments.spec.js
Normal file
@ -0,0 +1,61 @@
|
||||
import Factory from '../seed/factories'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { host, login } from '../jest/helpers'
|
||||
|
||||
const factory = Factory()
|
||||
let client
|
||||
let variables
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', {
|
||||
email: 'test@example.org',
|
||||
password: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('CreateComment', () => {
|
||||
const mutation = `
|
||||
mutation($postId: ID, $content: String!) {
|
||||
CreateComment(postId: $postId, content: $content) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
variables = {
|
||||
postId: 'p1',
|
||||
content: 'I\'m not authorised to comment'
|
||||
}
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(mutation, variables)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({ email: 'test@example.org', password: '1234' })
|
||||
client = new GraphQLClient(host, { headers })
|
||||
})
|
||||
|
||||
it('creates a post', async () => {
|
||||
variables = {
|
||||
postId: 'p1',
|
||||
content: 'I\'m authorised to comment'
|
||||
}
|
||||
const expected = {
|
||||
CreateComment: {
|
||||
content: 'I\'m authorised to comment'
|
||||
}
|
||||
}
|
||||
|
||||
await expect(client.request(mutation, variables)).resolves.toMatchObject(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -109,11 +109,11 @@ describe('disable', () => {
|
||||
await factory.authenticateAs({ email: 'commenter@example.org', password: '1234' })
|
||||
await Promise.all([
|
||||
factory.create('Post', { id: 'p3' }),
|
||||
factory.create('Comment', { id: 'c47' })
|
||||
factory.create('Comment', { id: 'c47', postId: 'p3', content: 'this comment was created for this post' })
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' }),
|
||||
factory.relate('Comment', 'Post', { from: 'c47', to: 'p3' })
|
||||
factory.relate('Comment', 'Author', { from: 'u45', to: 'c47' })
|
||||
])
|
||||
}
|
||||
})
|
||||
@ -286,8 +286,7 @@ describe('enable', () => {
|
||||
factory.create('Comment', { id: 'c456' })
|
||||
])
|
||||
await Promise.all([
|
||||
factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' }),
|
||||
factory.relate('Comment', 'Post', { from: 'c456', to: 'p9' })
|
||||
factory.relate('Comment', 'Author', { from: 'u123', to: 'c456' })
|
||||
])
|
||||
|
||||
const disableMutation = `
|
||||
|
||||
@ -3,7 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
export default {
|
||||
Mutation: {
|
||||
CreateSocialMedia: async (object, params, context, resolveInfo) => {
|
||||
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, true)
|
||||
const socialMedia = await neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
const session = context.driver.session()
|
||||
await session.run(
|
||||
`MATCH (owner:User {id: $userId}), (socialMedia:SocialMedia {id: $socialMediaId})
|
||||
|
||||
@ -16,6 +16,7 @@ type Query {
|
||||
LIMIT $limit
|
||||
"""
|
||||
)
|
||||
CommentByPost(postId: ID!): [Comment]!
|
||||
}
|
||||
type Mutation {
|
||||
# Get a JWT Token for the given Email and password
|
||||
@ -210,6 +211,7 @@ type Post {
|
||||
type Comment {
|
||||
id: ID!
|
||||
activityId: String
|
||||
postId: ID
|
||||
author: User @relation(name: "WROTE", direction: "IN")
|
||||
content: String!
|
||||
contentExcerpt: String
|
||||
|
||||
@ -4,6 +4,7 @@ import uuid from 'uuid/v4'
|
||||
export default function (params) {
|
||||
const {
|
||||
id = uuid(),
|
||||
postId = 'p6',
|
||||
content = [
|
||||
faker.lorem.sentence(),
|
||||
faker.lorem.sentence()
|
||||
@ -12,12 +13,12 @@ export default function (params) {
|
||||
|
||||
return {
|
||||
mutation: `
|
||||
mutation($id: ID!, $content: String!) {
|
||||
CreateComment(id: $id, content: $content) {
|
||||
mutation($id: ID!, $postId: ID, $content: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $content) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id, content }
|
||||
variables: { id, postId, content }
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,45 +189,33 @@ import Factory from './factories'
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
f.create('Comment', { id: 'c1' }),
|
||||
f.create('Comment', { id: 'c2' }),
|
||||
f.create('Comment', { id: 'c3' }),
|
||||
f.create('Comment', { id: 'c4' }),
|
||||
f.create('Comment', { id: 'c5' }),
|
||||
f.create('Comment', { id: 'c6' }),
|
||||
f.create('Comment', { id: 'c7' }),
|
||||
f.create('Comment', { id: 'c8' }),
|
||||
f.create('Comment', { id: 'c9' }),
|
||||
f.create('Comment', { id: 'c10' }),
|
||||
f.create('Comment', { id: 'c11' }),
|
||||
f.create('Comment', { id: 'c12' })
|
||||
f.create('Comment', { id: 'c1', postId: 'p1' }),
|
||||
f.create('Comment', { id: 'c2', postId: 'p1' }),
|
||||
f.create('Comment', { id: 'c3', postId: 'p3' }),
|
||||
f.create('Comment', { id: 'c4', postId: 'p2' }),
|
||||
f.create('Comment', { id: 'c5', postId: 'p3' }),
|
||||
f.create('Comment', { id: 'c6', postId: 'p4' }),
|
||||
f.create('Comment', { id: 'c7', postId: 'p2' }),
|
||||
f.create('Comment', { id: 'c8', postId: 'p15' }),
|
||||
f.create('Comment', { id: 'c9', postId: 'p15' }),
|
||||
f.create('Comment', { id: 'c10', postId: 'p15' }),
|
||||
f.create('Comment', { id: 'c11', postId: 'p15' }),
|
||||
f.create('Comment', { id: 'c12', postId: 'p15' })
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
f.relate('Comment', 'Author', { from: 'u3', to: 'c1' }),
|
||||
f.relate('Comment', 'Post', { from: 'c1', to: 'p1' }),
|
||||
f.relate('Comment', 'Author', { from: 'u1', to: 'c2' }),
|
||||
f.relate('Comment', 'Post', { from: 'c2', to: 'p1' }),
|
||||
f.relate('Comment', 'Author', { from: 'u1', to: 'c3' }),
|
||||
f.relate('Comment', 'Post', { from: 'c3', to: 'p3' }),
|
||||
f.relate('Comment', 'Author', { from: 'u4', to: 'c4' }),
|
||||
f.relate('Comment', 'Post', { from: 'c4', to: 'p2' }),
|
||||
f.relate('Comment', 'Author', { from: 'u4', to: 'c5' }),
|
||||
f.relate('Comment', 'Post', { from: 'c5', to: 'p3' }),
|
||||
f.relate('Comment', 'Author', { from: 'u3', to: 'c6' }),
|
||||
f.relate('Comment', 'Post', { from: 'c6', to: 'p4' }),
|
||||
f.relate('Comment', 'Author', { from: 'u2', to: 'c7' }),
|
||||
f.relate('Comment', 'Post', { from: 'c7', to: 'p2' }),
|
||||
f.relate('Comment', 'Author', { from: 'u5', to: 'c8' }),
|
||||
f.relate('Comment', 'Post', { from: 'c8', to: 'p15' }),
|
||||
f.relate('Comment', 'Author', { from: 'u6', to: 'c9' }),
|
||||
f.relate('Comment', 'Post', { from: 'c9', to: 'p15' }),
|
||||
f.relate('Comment', 'Author', { from: 'u7', to: 'c10' }),
|
||||
f.relate('Comment', 'Post', { from: 'c10', to: 'p15' }),
|
||||
f.relate('Comment', 'Author', { from: 'u5', to: 'c11' }),
|
||||
f.relate('Comment', 'Post', { from: 'c11', to: 'p15' }),
|
||||
f.relate('Comment', 'Author', { from: 'u6', to: 'c12' }),
|
||||
f.relate('Comment', 'Post', { from: 'c12', to: 'p15' })
|
||||
f.relate('Comment', 'Author', { from: 'u6', to: 'c12' })
|
||||
])
|
||||
|
||||
const disableMutation = 'mutation($id: ID!) { disable(id: $id) }'
|
||||
|
||||
23
cypress/integration/common/post.js
Normal file
23
cypress/integration/common/post.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { When, Then } from 'cypress-cucumber-preprocessor/steps'
|
||||
|
||||
When('I should be able to post a comment', () => {
|
||||
cy.get('.ProseMirror')
|
||||
.type('This is a comment')
|
||||
.get('.ds-form')
|
||||
.submit()
|
||||
.get('button')
|
||||
.contains('Submit Comment')
|
||||
.click()
|
||||
.get('.iziToast-message')
|
||||
.contains('Comment Submitted')
|
||||
})
|
||||
|
||||
Then('I should see my comment', () => {
|
||||
cy.get('div.comment p')
|
||||
.should('contain', 'This is a comment')
|
||||
})
|
||||
|
||||
Then('the editor should be cleared', () => {
|
||||
cy.get('.ProseMirror p')
|
||||
.should('have.class', 'is-empty')
|
||||
})
|
||||
17
cypress/integration/post/Comment.feature
Normal file
17
cypress/integration/post/Comment.feature
Normal file
@ -0,0 +1,17 @@
|
||||
Feature: Post Comment
|
||||
As a user
|
||||
I want to comment on contributions of others
|
||||
To be able to express my thoughts and emotions about these, discuss, and add give further information.
|
||||
|
||||
Background:
|
||||
Given we have the following posts in our database:
|
||||
| id | title | slug |
|
||||
| bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
|
||||
And I have a user account
|
||||
And I am logged in
|
||||
|
||||
Scenario: Comment creation
|
||||
Given I visit "post/bWBjpkTKZp/101-essays"
|
||||
Then I should be able to post a comment
|
||||
And I should see my comment
|
||||
And the editor should be cleared
|
||||
13
webapp/graphql/CommentQuery.js
Normal file
13
webapp/graphql/CommentQuery.js
Normal file
@ -0,0 +1,13 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default app => {
|
||||
return gql(`
|
||||
query CommentByPost($postId: ID!) {
|
||||
CommentByPost(postId: $postId, orderBy: createdAt_desc) {
|
||||
id
|
||||
contentExcerpt
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
@ -111,7 +111,9 @@
|
||||
},
|
||||
"takeAction": {
|
||||
"name": "Take action"
|
||||
}
|
||||
},
|
||||
"submitComment": "Submit Comment",
|
||||
"commentSubmitted": "Comment Submitted"
|
||||
},
|
||||
"quotes": {
|
||||
"african": {
|
||||
|
||||
@ -96,26 +96,72 @@
|
||||
<ds-space margin="small" />
|
||||
<!-- Comments -->
|
||||
<ds-section slot="footer">
|
||||
<h3 style="margin-top: 0;">
|
||||
<span>
|
||||
<ds-icon name="comments" />
|
||||
<ds-tag
|
||||
v-if="post.comments"
|
||||
style="margin-top: -4px; margin-left: -12px; position: absolute;"
|
||||
color="primary"
|
||||
size="small"
|
||||
round
|
||||
>{{ post.commentsCount }}</ds-tag> Comments
|
||||
</span>
|
||||
</h3>
|
||||
<ds-flex>
|
||||
<ds-flex-item width="20%">
|
||||
<h3 style="margin-top: 0;">
|
||||
<span>
|
||||
<ds-icon name="comments" />
|
||||
<ds-tag
|
||||
v-if="comments"
|
||||
style="margin-top: -4px; margin-left: -12px; position: absolute;"
|
||||
color="primary"
|
||||
size="small"
|
||||
round
|
||||
>{{ post.commentsCount }}</ds-tag> Comments
|
||||
</span>
|
||||
</h3>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item width="80%">
|
||||
<ds-form
|
||||
ref="commentForm"
|
||||
v-model="form"
|
||||
:schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template slot-scope="{ errors }">
|
||||
<ds-card>
|
||||
<no-ssr>
|
||||
<hc-editor
|
||||
:value="form.content"
|
||||
@input="updateEditorContent"
|
||||
/>
|
||||
</no-ssr>
|
||||
<ds-space />
|
||||
<ds-flex>
|
||||
<ds-flex-item width="50%" />
|
||||
<ds-flex-item width="20%">
|
||||
<ds-button
|
||||
:disabled="loading || disabled"
|
||||
ghost
|
||||
@click.prevent="clearEditor"
|
||||
>
|
||||
{{ $t('actions.cancel') }}
|
||||
</ds-button>
|
||||
</ds-flex-item>
|
||||
<ds-flex-item width="20%">
|
||||
<ds-button
|
||||
type="submit"
|
||||
:loading="loading"
|
||||
:disabled="disabled || errors"
|
||||
primary
|
||||
>
|
||||
{{ $t('post.submitComment') }}
|
||||
</ds-button>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
</ds-card>
|
||||
</template>
|
||||
</ds-form>
|
||||
</ds-flex-item>
|
||||
</ds-flex>
|
||||
<ds-space margin-bottom="large" />
|
||||
<div
|
||||
v-if="post.comments"
|
||||
v-if="comments && comments.length"
|
||||
id="comments"
|
||||
class="comments"
|
||||
>
|
||||
<comment
|
||||
v-for="comment in post.comments"
|
||||
v-for="comment in comments"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
/>
|
||||
@ -139,6 +185,7 @@ import HcUser from '~/components/User'
|
||||
import HcShoutButton from '~/components/ShoutButton.vue'
|
||||
import HcEmpty from '~/components/Empty.vue'
|
||||
import Comment from '~/components/Comment.vue'
|
||||
import HcEditor from '~/components/Editor'
|
||||
|
||||
export default {
|
||||
transition: {
|
||||
@ -152,7 +199,8 @@ export default {
|
||||
HcShoutButton,
|
||||
HcEmpty,
|
||||
Comment,
|
||||
ContentMenu
|
||||
ContentMenu,
|
||||
HcEditor
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
@ -162,14 +210,26 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
post: null,
|
||||
comments: null,
|
||||
ready: false,
|
||||
title: 'loading'
|
||||
title: 'loading',
|
||||
loading: false,
|
||||
disabled: false,
|
||||
form: {
|
||||
content: ''
|
||||
},
|
||||
formSchema: {
|
||||
content: { required: true, min: 3 }
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
Post(post) {
|
||||
this.post = post[0] || {}
|
||||
this.title = this.post.title
|
||||
},
|
||||
CommentByPost(comments) {
|
||||
this.comments = comments || []
|
||||
}
|
||||
},
|
||||
async asyncData(context) {
|
||||
@ -278,6 +338,61 @@ export default {
|
||||
methods: {
|
||||
isAuthor(id) {
|
||||
return this.$store.getters['auth/user'].id === id
|
||||
},
|
||||
updateEditorContent(value) {
|
||||
this.$refs.commentForm.update('content', value)
|
||||
},
|
||||
clearEditor() {
|
||||
this.loading = false
|
||||
this.disabled = false
|
||||
this.form.content = ' '
|
||||
},
|
||||
addComment(comment) {
|
||||
this.$apollo.queries.CommentByPost.refetch()
|
||||
// this.post = { ...this.post, comments: [...this.post.comments, comment] }
|
||||
},
|
||||
handleSubmit() {
|
||||
const content = this.form.content
|
||||
this.form.content = ' '
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: gql`
|
||||
mutation($postId: ID, $content: String!) {
|
||||
CreateComment(postId: $postId, content: $content) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
postId: this.post.id,
|
||||
content
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
this.addComment(res.data.CreateComment)
|
||||
this.loading = false
|
||||
this.disabled = false
|
||||
this.$toast.success(this.$t('post.commentSubmitted'))
|
||||
})
|
||||
.catch(err => {
|
||||
this.$toast.error(err.message)
|
||||
this.loading = false
|
||||
this.disabled = false
|
||||
})
|
||||
}
|
||||
},
|
||||
apollo: {
|
||||
CommentByPost: {
|
||||
query() {
|
||||
return require('~/graphql/CommentQuery.js').default(this)
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
postId: this.post.id
|
||||
}
|
||||
},
|
||||
fetchPolicy: 'cache-and-network'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user