mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #2608 from Human-Connection/1680-Direct_answer_on_Comment
feat: 🍰 Direct Reply On Comment
This commit is contained in:
commit
4170e62f7a
@ -55,7 +55,19 @@ export default function create() {
|
||||
if (authorId) author = await neodeInstance.find('User', authorId)
|
||||
author = author || (await factoryInstance.create('User'))
|
||||
const post = await neodeInstance.create('Post', args)
|
||||
|
||||
const { commentContent } = args
|
||||
let comment
|
||||
delete args.commentContent
|
||||
if (commentContent)
|
||||
comment = await factoryInstance.create('Comment', {
|
||||
contentExcerpt: commentContent,
|
||||
post,
|
||||
author,
|
||||
})
|
||||
|
||||
await post.relateTo(author, 'author')
|
||||
if (comment) await post.relateTo(comment, 'comments')
|
||||
|
||||
if (args.pinned) {
|
||||
args.pinnedAt = args.pinnedAt || new Date().toISOString()
|
||||
|
||||
@ -41,6 +41,16 @@ export default {
|
||||
language: { type: 'string', allow: [null] },
|
||||
imageBlurred: { type: 'boolean', default: false },
|
||||
imageAspectRatio: { type: 'float', default: 1.0 },
|
||||
comments: {
|
||||
type: 'relationship',
|
||||
relationship: 'COMMENTS',
|
||||
target: 'Comment',
|
||||
direction: 'in',
|
||||
properties: {
|
||||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
updatedAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||
},
|
||||
},
|
||||
pinned: { type: 'boolean', default: null, valid: [null, true] },
|
||||
pinnedAt: { type: 'string', isoDate: true },
|
||||
}
|
||||
|
||||
@ -17,8 +17,13 @@ Then("I click on the {string} button", text => {
|
||||
.click();
|
||||
});
|
||||
|
||||
Then("I click on the reply button", () => {
|
||||
cy.get(".reply-button")
|
||||
.click();
|
||||
});
|
||||
|
||||
Then("my comment should be successfully created", () => {
|
||||
cy.get(".iziToast-message").contains("Comment Submitted");
|
||||
cy.get(".iziToast-message").contains("Comment submitted!");
|
||||
});
|
||||
|
||||
Then("I should see my comment", () => {
|
||||
@ -45,6 +50,12 @@ Then("the editor should be cleared", () => {
|
||||
cy.get(".ProseMirror p").should("have.class", "is-empty");
|
||||
});
|
||||
|
||||
Then("it should create a mention in the CommentForm", () => {
|
||||
cy.get(".ProseMirror a")
|
||||
.should('have.class', 'mention')
|
||||
.should('contain', '@peter-pan')
|
||||
})
|
||||
|
||||
When("I open the content menu of post {string}", (title)=> {
|
||||
cy.contains('.post-card', title)
|
||||
.find('.content-menu .base-button')
|
||||
|
||||
@ -4,10 +4,10 @@ Feature: Post Comment
|
||||
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
|
||||
Given I have a user account
|
||||
And we have the following posts in our database:
|
||||
| id | title | slug | authorId | commentContent |
|
||||
| bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | id-of-peter-pan | @peter-pan reply to me |
|
||||
And I am logged in
|
||||
|
||||
Scenario: Comment creation
|
||||
@ -36,3 +36,8 @@ Feature: Post Comment
|
||||
Then my comment should be successfully created
|
||||
And I should see an abreviated version of my comment
|
||||
And the editor should be cleared
|
||||
|
||||
Scenario: Direct reply to Comment
|
||||
Given I visit "post/bWBjpkTKZp/101-essays"
|
||||
And I click on the reply button
|
||||
Then it should create a mention in the CommentForm
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
/* globals Cypress cy */
|
||||
import "cypress-file-upload";
|
||||
import helpers from "./helpers";
|
||||
import users from "../fixtures/users.json";
|
||||
import { GraphQLClient, request } from 'graphql-request'
|
||||
import { gql } from '../../backend/src/helpers/jest'
|
||||
import config from '../../backend/src/config'
|
||||
|
||||
5
webapp/assets/_new/icons/svgs/level-down.svg
Executable file
5
webapp/assets/_new/icons/svgs/level-down.svg
Executable file
@ -0,0 +1,5 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>level-down</title>
|
||||
<path d="M5 5h17v19.063l4.281-4.281 1.438 1.438-6 6-0.719 0.688-0.719-0.688-6-6 1.438-1.438 4.281 4.281v-17.063h-15v-2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@ -1,17 +1,15 @@
|
||||
import { config, shallowMount } from '@vue/test-utils'
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import Comment from './Comment.vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.directive('scrollTo', jest.fn())
|
||||
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||
|
||||
describe('Comment.vue', () => {
|
||||
let propsData
|
||||
let mocks
|
||||
let getters
|
||||
let wrapper
|
||||
let Wrapper
|
||||
let propsData, mocks, stubs, getters, wrapper, Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
@ -39,6 +37,9 @@ describe('Comment.vue', () => {
|
||||
}),
|
||||
},
|
||||
}
|
||||
stubs = {
|
||||
ContentViewer: true,
|
||||
}
|
||||
getters = {
|
||||
'auth/user': () => {
|
||||
return {}
|
||||
@ -47,18 +48,19 @@ describe('Comment.vue', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('shallowMount', () => {
|
||||
describe('mount', () => {
|
||||
beforeEach(jest.useFakeTimers)
|
||||
|
||||
Wrapper = () => {
|
||||
const store = new Vuex.Store({
|
||||
getters,
|
||||
})
|
||||
return shallowMount(Comment, {
|
||||
return mount(Comment, {
|
||||
store,
|
||||
propsData,
|
||||
mocks,
|
||||
localVue,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
|
||||
@ -68,6 +70,7 @@ describe('Comment.vue', () => {
|
||||
id: '2',
|
||||
contentExcerpt: 'Hello I am a comment content',
|
||||
content: 'Hello I am comment content',
|
||||
author: { id: 'commentAuthorId', slug: 'ogerly' },
|
||||
}
|
||||
})
|
||||
|
||||
@ -199,6 +202,24 @@ describe('Comment.vue', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('click reply button', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = Wrapper()
|
||||
await wrapper.find('.reply-button').trigger('click')
|
||||
})
|
||||
|
||||
it('emits "reply"', () => {
|
||||
expect(wrapper.emitted('reply')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'commentAuthorId',
|
||||
slug: 'ogerly',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -54,6 +54,15 @@
|
||||
</button>
|
||||
</div>
|
||||
<ds-space margin-bottom="small" />
|
||||
<base-button
|
||||
:title="this.$t('post.comment.reply')"
|
||||
icon="level-down"
|
||||
class="reply-button"
|
||||
circle
|
||||
size="small"
|
||||
v-scroll-to="'.editor'"
|
||||
@click.prevent="reply"
|
||||
></base-button>
|
||||
</ds-card>
|
||||
</div>
|
||||
</template>
|
||||
@ -67,6 +76,7 @@ import ContentViewer from '~/components/Editor/ContentViewer'
|
||||
import CommentForm from '~/components/CommentForm/CommentForm'
|
||||
import CommentMutations from '~/graphql/CommentMutations'
|
||||
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
|
||||
import BaseButton from '~/components/_new/generic/BaseButton/BaseButton'
|
||||
|
||||
export default {
|
||||
mixins: [scrollToAnchor],
|
||||
@ -86,6 +96,7 @@ export default {
|
||||
ContentMenu,
|
||||
ContentViewer,
|
||||
CommentForm,
|
||||
BaseButton,
|
||||
},
|
||||
props: {
|
||||
routeHash: { type: String, default: () => '' },
|
||||
@ -105,7 +116,6 @@ export default {
|
||||
if (this.isLongComment && this.isCollapsed) {
|
||||
return this.$filters.truncate(this.comment.content, COMMENT_TRUNCATE_TO_LENGTH)
|
||||
}
|
||||
|
||||
return this.comment.content
|
||||
},
|
||||
displaysComment() {
|
||||
@ -141,6 +151,10 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reply() {
|
||||
const message = { slug: this.comment.author.slug, id: this.comment.author.id }
|
||||
this.$emit('reply', message)
|
||||
},
|
||||
checkAnchor(anchor) {
|
||||
return `#${this.anchor}` === anchor
|
||||
},
|
||||
@ -193,6 +207,14 @@ export default {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.reply-button {
|
||||
float: right;
|
||||
top: 0px;
|
||||
}
|
||||
.reply-button:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
0% {
|
||||
border: 1px solid $color-primary;
|
||||
|
||||
@ -51,6 +51,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reply(message) {
|
||||
this.$refs.editor.insertReply(message)
|
||||
},
|
||||
updateEditorContent(value) {
|
||||
const sanitizedContent = this.$filters.removeHtml(value, false)
|
||||
if (!this.update) {
|
||||
@ -133,8 +136,8 @@ export default {
|
||||
query() {
|
||||
return minimisedUserQuery()
|
||||
},
|
||||
result(result) {
|
||||
this.users = result.data.User
|
||||
update({ User }) {
|
||||
this.users = User
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import CommentList from './CommentList'
|
||||
import Comment from '~/components/Comment/Comment'
|
||||
import Vuex from 'vuex'
|
||||
import Vue from 'vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
localVue.filter('truncate', string => string)
|
||||
localVue.directive('scrollTo', jest.fn())
|
||||
|
||||
config.stubs['v-popover'] = '<span><slot /></span>'
|
||||
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||
@ -97,5 +100,27 @@ describe('CommentList.vue', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Comment', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('Comment emitted reply()', () => {
|
||||
wrapper.find(Comment).vm.$emit('reply', {
|
||||
id: 'commentAuthorId',
|
||||
slug: 'ogerly',
|
||||
})
|
||||
Vue.nextTick()
|
||||
expect(wrapper.emitted('reply')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 'commentAuthorId',
|
||||
slug: 'ogerly',
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -12,9 +12,11 @@
|
||||
:comment="comment"
|
||||
:post="post"
|
||||
:routeHash="routeHash"
|
||||
class="comment-tag"
|
||||
@deleteComment="updateCommentList"
|
||||
@updateComment="updateCommentList"
|
||||
@toggleNewCommentForm="toggleNewCommentForm"
|
||||
@reply="reply"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -35,6 +37,9 @@ export default {
|
||||
post: { type: Object, default: () => {} },
|
||||
},
|
||||
methods: {
|
||||
reply(message) {
|
||||
this.$emit('reply', message)
|
||||
},
|
||||
checkAnchor(anchor) {
|
||||
return anchor === '#comments'
|
||||
},
|
||||
|
||||
@ -141,7 +141,6 @@ export default {
|
||||
methods: {
|
||||
openSuggestionList({ items, query, range, command, virtualNode }, suggestionType) {
|
||||
this.suggestionType = suggestionType
|
||||
|
||||
this.query = this.sanitizeQuery(query)
|
||||
this.filteredItems = items
|
||||
this.suggestionRange = range
|
||||
@ -237,6 +236,9 @@ export default {
|
||||
const content = e.getHTML()
|
||||
this.$emit('input', content)
|
||||
},
|
||||
insertReply(message) {
|
||||
this.editor.commands.mention({ id: message.id, label: message.slug })
|
||||
},
|
||||
toggleLinkInput(attrs, element) {
|
||||
if (!this.isLinkInputActive && attrs && element) {
|
||||
this.$refs.linkInput.linkUrl = attrs.href
|
||||
|
||||
@ -105,7 +105,7 @@ export default {
|
||||
postId: this.post.id,
|
||||
}
|
||||
},
|
||||
result({ data: { PostsEmotionsByCurrentUser } }) {
|
||||
update({ PostsEmotionsByCurrentUser }) {
|
||||
this.selectedEmotions = PostsEmotionsByCurrentUser
|
||||
},
|
||||
},
|
||||
|
||||
@ -279,8 +279,9 @@
|
||||
},
|
||||
"comment": {
|
||||
"submit": "Kommentiere",
|
||||
"submitted": "Kommentar gesendet",
|
||||
"updated": "Änderungen gespeichert"
|
||||
"submitted": "Kommentar gesendet!",
|
||||
"updated": "Änderungen gespeichert",
|
||||
"reply": "Antworten"
|
||||
},
|
||||
"edited": "bearbeitet"
|
||||
},
|
||||
|
||||
@ -444,8 +444,9 @@
|
||||
},
|
||||
"comment": {
|
||||
"submit": "Comment",
|
||||
"submitted": "Comment Submitted",
|
||||
"updated": "Changes Saved"
|
||||
"submitted": "Comment submitted!",
|
||||
"updated": "Changes saved!",
|
||||
"reply": "Reply"
|
||||
},
|
||||
"edited": "edited"
|
||||
},
|
||||
|
||||
@ -1,29 +1,33 @@
|
||||
import { config, shallowMount } from '@vue/test-utils'
|
||||
import PostSlug from './index.vue'
|
||||
import { config, mount } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
const localVue = global.localVue
|
||||
import PostSlug from './index.vue'
|
||||
import CommentList from '~/components/CommentList/CommentList'
|
||||
|
||||
config.stubs['client-only'] = '<span><slot /></span>'
|
||||
config.stubs['nuxt-link'] = '<span><slot /></span>'
|
||||
config.stubs['router-link'] = '<span><slot /></span>'
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.directive('scrollTo', jest.fn())
|
||||
|
||||
describe('PostSlug', () => {
|
||||
let wrapper
|
||||
let Wrapper
|
||||
let store
|
||||
let mocks
|
||||
let store, propsData, mocks, stubs, wrapper, Wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Vuex.Store({
|
||||
getters: {
|
||||
'auth/user': () => {
|
||||
return {}
|
||||
return { id: '1stUser' }
|
||||
},
|
||||
'auth/isModerator': () => false,
|
||||
},
|
||||
})
|
||||
propsData = {}
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
$filters: {
|
||||
truncate: a => a,
|
||||
removeHtml: a => a,
|
||||
},
|
||||
$route: {
|
||||
hash: '',
|
||||
@ -40,38 +44,53 @@ describe('PostSlug', () => {
|
||||
},
|
||||
$apollo: {
|
||||
mutate: jest.fn().mockResolvedValue(),
|
||||
query: jest.fn().mockResolvedValue({ data: { PostEmotionsCountByEmotion: {} } }),
|
||||
},
|
||||
$scrollTo: jest.fn(),
|
||||
}
|
||||
stubs = {
|
||||
HcEditor: { render: () => {}, methods: { insertReply: jest.fn(() => null) } },
|
||||
ContentViewer: true,
|
||||
}
|
||||
jest.useFakeTimers()
|
||||
wrapper = Wrapper()
|
||||
wrapper.setData({
|
||||
post: {
|
||||
id: '1',
|
||||
author: {
|
||||
id: '1stUser',
|
||||
},
|
||||
comments: [
|
||||
{
|
||||
id: 'comment134',
|
||||
contentExcerpt: 'this is a comment',
|
||||
content: 'this is a comment',
|
||||
author: {
|
||||
id: '1stUser',
|
||||
slug: '1st-user',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ready: true,
|
||||
})
|
||||
})
|
||||
|
||||
describe('shallowMount', () => {
|
||||
describe('mount', () => {
|
||||
Wrapper = () => {
|
||||
return shallowMount(PostSlug, {
|
||||
return mount(PostSlug, {
|
||||
store,
|
||||
mocks,
|
||||
localVue,
|
||||
propsData,
|
||||
stubs,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(jest.useFakeTimers)
|
||||
|
||||
describe('test Post callbacks', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.setData({
|
||||
post: {
|
||||
id: 'p23',
|
||||
name: 'It is a post',
|
||||
author: {
|
||||
id: 'u1',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('deletion of Post from Page by invoking "deletePostCallback()"', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.deletePostCallback()
|
||||
beforeEach(async () => {
|
||||
await wrapper.vm.deletePostCallback()
|
||||
})
|
||||
|
||||
describe('after timeout', () => {
|
||||
@ -91,5 +110,18 @@ describe('PostSlug', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('reply method called when emitted reply received', () => {
|
||||
it('CommentList', async () => {
|
||||
wrapper.find(CommentList).vm.$emit('reply', {
|
||||
id: 'commentAuthorId',
|
||||
slug: 'ogerly',
|
||||
})
|
||||
expect(stubs.HcEditor.methods.insertReply).toHaveBeenCalledWith({
|
||||
id: 'commentAuthorId',
|
||||
slug: 'ogerly',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -84,14 +84,16 @@
|
||||
</ds-space>
|
||||
<!-- Comments -->
|
||||
<ds-section slot="footer">
|
||||
<hc-comment-list
|
||||
<comment-list
|
||||
:post="post"
|
||||
:routeHash="$route.hash"
|
||||
@toggleNewCommentForm="toggleNewCommentForm"
|
||||
@reply="reply"
|
||||
/>
|
||||
<ds-space margin-bottom="large" />
|
||||
<comment-form
|
||||
v-if="showNewCommentForm && !post.author.blocked"
|
||||
ref="commentForm"
|
||||
:post="post"
|
||||
@createComment="createComment"
|
||||
/>
|
||||
@ -114,7 +116,7 @@ import ContentMenu from '~/components/ContentMenu/ContentMenu'
|
||||
import UserTeaser from '~/components/UserTeaser/UserTeaser'
|
||||
import HcShoutButton from '~/components/ShoutButton.vue'
|
||||
import CommentForm from '~/components/CommentForm/CommentForm'
|
||||
import HcCommentList from '~/components/CommentList/CommentList'
|
||||
import CommentList from '~/components/CommentList/CommentList'
|
||||
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
|
||||
import PostQuery from '~/graphql/PostQuery'
|
||||
import HcEmotions from '~/components/Emotions/Emotions'
|
||||
@ -133,7 +135,7 @@ export default {
|
||||
HcShoutButton,
|
||||
ContentMenu,
|
||||
CommentForm,
|
||||
HcCommentList,
|
||||
CommentList,
|
||||
HcEmotions,
|
||||
ContentViewer,
|
||||
},
|
||||
@ -170,6 +172,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reply(message) {
|
||||
this.$refs.commentForm && this.$refs.commentForm.reply(message)
|
||||
},
|
||||
isAuthor(id) {
|
||||
return this.$store.getters['auth/user'].id === id
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user