Merge pull request #1537 from Human-Connection/1455-fix-update-comment-list

🍰 Fixes a create and update comment problem in the comments list
This commit is contained in:
mattwr18 2019-09-12 18:36:30 +02:00 committed by GitHub
commit fb2cade2f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 311 additions and 224 deletions

View File

@ -33,7 +33,11 @@ describe('Comment.vue', () => {
},
$apollo: {
mutate: jest.fn().mockResolvedValue({
data: { DeleteComment: { id: 'it-is-the-deleted-comment' } },
data: {
DeleteComment: {
id: 'it-is-the-deleted-comment',
},
},
}),
},
}
@ -125,7 +129,11 @@ describe('Comment.vue', () => {
it('emits "deleteComment"', () => {
expect(wrapper.emitted('deleteComment')).toEqual([
[{ id: 'it-is-the-deleted-comment' }],
[
{
id: 'it-is-the-deleted-comment',
},
],
])
})
@ -138,6 +146,30 @@ describe('Comment.vue', () => {
})
})
})
describe('test update comment', () => {
beforeEach(() => {
wrapper = Wrapper()
})
describe('with a given comment', () => {
beforeEach(async () => {
await wrapper.vm.updateComment({
id: 'it-is-the-updated-comment',
})
})
it('emits "updateComment"', () => {
expect(wrapper.emitted('updateComment')).toEqual([
[
{
id: 'it-is-the-updated-comment',
},
],
])
})
})
})
})
})
})

View File

@ -13,26 +13,29 @@
<ds-card :id="`commentId-${comment.id}`">
<ds-space margin-bottom="small">
<hc-user :user="author" :date-time="comment.createdAt" />
<!-- Content Menu (can open Modals) -->
<client-only>
<content-menu
v-show="!openEditCommentMenu"
placement="bottom-end"
resource-type="comment"
:resource="comment"
:modalsData="menuModalsData"
style="float-right"
:is-owner="isAuthor(author.id)"
@showEditCommentMenu="editCommentMenu"
/>
</client-only>
</ds-space>
<!-- Content Menu (can open Modals) -->
<client-only>
<content-menu
placement="bottom-end"
resource-type="comment"
:resource="comment"
:modalsData="menuModalsData"
style="float-right"
:is-owner="isAuthor(author.id)"
@showEditCommentMenu="editCommentMenu"
/>
</client-only>
<ds-space margin-bottom="small" />
<div v-if="openEditCommentMenu">
<hc-edit-comment-form
:comment="comment"
<hc-comment-form
:update="true"
:post="post"
:comment="comment"
@showEditCommentMenu="editCommentMenu"
@updateComment="updateComment"
/>
</div>
<div v-show="!openEditCommentMenu">
@ -66,7 +69,7 @@ import { mapGetters } from 'vuex'
import HcUser from '~/components/User/User'
import ContentMenu from '~/components/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import HcEditCommentForm from '~/components/EditCommentForm/EditCommentForm'
import HcCommentForm from '~/components/CommentForm/CommentForm'
import CommentMutations from '~/graphql/CommentMutations'
export default {
@ -80,7 +83,7 @@ export default {
HcUser,
ContentMenu,
ContentViewer,
HcEditCommentForm,
HcCommentForm,
},
props: {
post: { type: Object, default: () => {} },
@ -136,6 +139,9 @@ export default {
editCommentMenu(showMenu) {
this.openEditCommentMenu = showMenu
},
updateComment(comment) {
this.$emit('updateComment', comment)
},
async deleteCommentCallback() {
try {
const {

View File

@ -12,8 +12,8 @@ describe('CommentForm.vue', () => {
let mocks
let wrapper
let propsData
let cancelBtn
let cancelMethodSpy
let closeMethodSpy
beforeEach(() => {
mocks = {
@ -21,79 +21,180 @@ describe('CommentForm.vue', () => {
$i18n: {
locale: () => 'en',
},
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
CreateComment: {
contentExcerpt: 'this is a comment',
},
},
})
.mockRejectedValue({
message: 'Ouch!',
}),
},
$toast: {
error: jest.fn(),
success: jest.fn(),
},
}
propsData = {
post: {
id: 1,
$filters: {
removeHtml: a => a,
},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(CommentForm, {
mocks,
localVue,
propsData,
describe('create comment', () => {
beforeEach(() => {
mocks = {
...mocks,
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
CreateComment: {
contentExcerpt: 'this is a comment',
},
},
})
.mockRejectedValue({
message: 'Ouch!',
}),
},
}
propsData = {
post: {
id: 'p001',
},
}
const Wrapper = () => {
return mount(CommentForm, {
mocks,
localVue,
propsData,
})
}
wrapper = Wrapper()
cancelMethodSpy = jest.spyOn(wrapper.vm, 'clear')
})
}
beforeEach(() => {
wrapper = Wrapper()
cancelMethodSpy = jest.spyOn(wrapper.vm, 'clear')
})
it('calls the apollo mutation when form is submitted', async () => {
wrapper.vm.updateEditorContent('this is a comment')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it('calls clear method when the cancel button is clicked', () => {
wrapper.vm.updateEditorContent('ok')
cancelBtn = wrapper.find('.cancelBtn')
cancelBtn.trigger('click')
expect(cancelMethodSpy).toHaveBeenCalledTimes(1)
})
describe('mutation resolves', () => {
beforeEach(async () => {
it('calls the apollo mutation when form is submitted', async () => {
wrapper.vm.updateEditorContent('this is a comment')
wrapper.find('form').trigger('submit')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it('shows a success toaster', async () => {
await mocks.$apollo.mutate
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
it('clears the editor', () => {
it('calls `clear` method when the cancel button is clicked', async () => {
wrapper.vm.updateEditorContent('ok')
await wrapper.find('.cancelBtn').trigger('submit')
expect(cancelMethodSpy).toHaveBeenCalledTimes(1)
})
describe('mutation fails', () => {
it('shows the error toaster', async () => {
await wrapper.find('form').trigger('submit')
describe('mutation resolves', () => {
beforeEach(async () => {
wrapper.vm.updateEditorContent('this is a comment')
wrapper.find('form').trigger('submit')
})
it('shows a success toaster', async () => {
await mocks.$apollo.mutate
expect(mocks.$toast.error).toHaveBeenCalledTimes(1)
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
it('clears the editor', () => {
expect(cancelMethodSpy).toHaveBeenCalledTimes(1)
})
describe('mutation fails', () => {
it('shows the error toaster', async () => {
await wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate
expect(mocks.$toast.error).toHaveBeenCalledTimes(1)
})
})
})
})
describe('update comment', () => {
beforeEach(() => {
mocks = {
...mocks,
$apollo: {
mutate: jest
.fn()
.mockResolvedValueOnce({
data: {
UpdateComment: {
contentExcerpt: 'this is a comment',
},
},
})
.mockRejectedValue({
message: 'Ouch!',
}),
},
}
propsData = {
update: true,
comment: {
id: 'c001',
},
}
const Wrapper = () => {
return mount(CommentForm, {
mocks,
localVue,
propsData,
})
}
wrapper = Wrapper()
closeMethodSpy = jest.spyOn(wrapper.vm, 'closeEditWindow')
})
describe('form submitted', () => {
it('calls the apollo mutation', async () => {
wrapper.vm.updateEditorContent('this is a comment')
await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledTimes(1)
})
it('calls `closeEditWindow` method', async () => {
wrapper.vm.updateEditorContent('ok')
await wrapper.find('form').trigger('submit')
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
})
it('emits `showEditCommentMenu` event', async () => {
wrapper.vm.updateEditorContent('ok')
await wrapper.find('form').trigger('submit')
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[false]])
})
})
describe('cancel button is clicked', () => {
it('calls `closeEditWindow` method', async () => {
wrapper.vm.updateEditorContent('ok')
await wrapper.find('.cancelBtn').trigger('submit')
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
})
it('emits `showEditCommentMenu` event', async () => {
wrapper.vm.updateEditorContent('ok')
await wrapper.find('.cancelBtn').trigger('submit')
expect(wrapper.emitted('showEditCommentMenu')).toEqual([[false]])
})
})
describe('mutation resolves', () => {
beforeEach(async () => {
wrapper.vm.updateEditorContent('this is a comment')
wrapper.find('form').trigger('submit')
})
it('shows a success toaster', async () => {
await mocks.$apollo.mutate
expect(mocks.$toast.success).toHaveBeenCalledTimes(1)
})
it('closes the editor', () => {
expect(closeMethodSpy).toHaveBeenCalledTimes(1)
})
describe('mutation fails', () => {
it('shows the error toaster', async () => {
await wrapper.find('form').trigger('submit')
await mocks.$apollo.mutate
expect(mocks.$toast.error).toHaveBeenCalledTimes(1)
})
})
})
})

View File

@ -2,18 +2,18 @@
<ds-form v-model="form" @submit="handleSubmit">
<template slot-scope="{ errors }">
<ds-card>
<hc-editor
ref="editor"
:users="users"
:hashtags="null"
:value="form.content"
@input="updateEditorContent"
/>
<!-- with client-only the content is not shown -->
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
<ds-space />
<ds-flex :gutter="{ base: 'small', md: 'small', sm: 'x-large', xs: 'x-large' }">
<ds-flex-item :width="{ base: '0%', md: '50%', sm: '0%', xs: '0%' }" />
<ds-flex-item :width="{ base: '40%', md: '20%', sm: '30%', xs: '30%' }">
<ds-button :disabled="disabled" ghost class="cancelBtn" @click.prevent="clear">
<ds-button
:disabled="disabled && !update"
ghost
class="cancelBtn"
@click.prevent="handleCancel"
>
{{ $t('actions.cancel') }}
</ds-button>
</ds-flex-item>
@ -29,9 +29,9 @@
</template>
<script>
import gql from 'graphql-tag'
import HcEditor from '~/components/Editor/Editor'
import PostQuery from '~/graphql/PostQuery'
import { COMMENT_MIN_LENGTH } from '../../constants/comment'
import { minimisedUserQuery } from '~/graphql/User'
import CommentMutations from '~/graphql/CommentMutations'
export default {
@ -39,55 +39,94 @@ export default {
HcEditor,
},
props: {
update: { type: Boolean, default: () => false },
post: { type: Object, default: () => {} },
comment: {
type: Object,
default: () => {},
},
},
data() {
return {
disabled: true,
loading: false,
form: {
content: '',
content: !this.update || !this.comment.content ? '' : this.comment.content,
},
users: [],
}
},
methods: {
updateEditorContent(value) {
const content = value.replace(/<(?:.|\n)*?>/gm, '').trim()
if (content.length < 1) {
this.disabled = true
const sanitizedContent = this.$filters.removeHtml(value, false)
if (!this.update) {
if (sanitizedContent.length < COMMENT_MIN_LENGTH) {
this.disabled = true
} else {
this.disabled = false
}
} else {
this.disabled = false
this.disabled =
value === this.comment.content || sanitizedContent.length < COMMENT_MIN_LENGTH
}
this.form.content = value
},
clear() {
this.$refs.editor.clear()
},
closeEditWindow() {
this.$emit('showEditCommentMenu', false)
},
handleCancel() {
if (!this.update) {
this.clear()
} else {
this.closeEditWindow()
}
},
handleSubmit() {
this.loading = true
this.disabled = true
this.$apollo
.mutate({
let mutateParams
if (!this.update) {
mutateParams = {
mutation: CommentMutations(this.$i18n).CreateComment,
variables: {
postId: this.post.id,
content: this.form.content,
},
update: async (store, { data: { CreateComment } }) => {
const data = await store.readQuery({
query: PostQuery(this.$i18n),
variables: { id: this.post.id },
})
data.Post[0].comments.push(CreateComment)
await store.writeQuery({ query: PostQuery(this.$i18n), data })
}
} else {
mutateParams = {
mutation: CommentMutations(this.$i18n).UpdateComment,
variables: {
id: this.comment.id,
content: this.form.content,
},
})
}
}
this.loading = true
this.disabled = true
this.$apollo
.mutate(mutateParams)
.then(res => {
this.loading = false
this.clear()
this.$toast.success(this.$t('post.comment.submitted'))
this.disabled = false
if (!this.update) {
const {
data: { CreateComment },
} = res
this.$emit('createComment', CreateComment)
this.clear()
this.$toast.success(this.$t('post.comment.submitted'))
this.disabled = false
} else {
const {
data: { UpdateComment },
} = res
this.$emit('updateComment', UpdateComment)
this.$toast.success(this.$t('post.comment.updated'))
this.disabled = false
this.closeEditWindow()
}
})
.catch(err => {
this.$toast.error(err.message)
@ -97,16 +136,7 @@ export default {
apollo: {
User: {
query() {
return gql`
{
User(orderBy: slug_asc) {
id
slug
name
avatar
}
}
`
return minimisedUserQuery()
},
result(result) {
this.users = result.data.User

View File

@ -22,7 +22,8 @@
:key="comment.id"
:comment="comment"
:post="post"
@deleteComment="deleteComment"
@deleteComment="updateCommentList"
@updateComment="updateCommentList"
/>
</div>
<hc-empty v-else name="empty" icon="messages" />
@ -41,9 +42,9 @@ export default {
post: { type: Object, default: () => {} },
},
methods: {
deleteComment(deleted) {
updateCommentList(updatedComment) {
this.post.comments = this.post.comments.map(comment => {
return comment.id === deleted.id ? deleted : comment
return comment.id === updatedComment.id ? updatedComment : comment
})
},
},

View File

@ -1,103 +0,0 @@
<template>
<ds-form v-model="form" @submit="handleSubmit">
<template slot-scope="{ errors }">
<ds-card>
<!-- with client-only the content is not shown -->
<hc-editor ref="editor" :users="users" :value="form.content" @input="updateEditorContent" />
<ds-space />
<ds-flex :gutter="{ base: 'small', md: 'small', sm: 'x-large', xs: 'x-large' }">
<ds-flex-item :width="{ base: '0%', md: '50%', sm: '0%', xs: '0%' }" />
<ds-flex-item :width="{ base: '40%', md: '20%', sm: '30%', xs: '30%' }">
<ds-button ghost class="cancelBtn" @click.prevent="closeEditWindow">
{{ $t('actions.cancel') }}
</ds-button>
</ds-flex-item>
<ds-flex-item :width="{ base: '40%', md: '20%', sm: '40%', xs: '40%' }">
<ds-button type="submit" :loading="loading" :disabled="disabled || errors" primary>
{{ $t('post.comment.submit') }}
</ds-button>
</ds-flex-item>
</ds-flex>
</ds-card>
</template>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
import HcEditor from '~/components/Editor/Editor'
import CommentMutations from '~/graphql/CommentMutations.js'
export default {
components: {
HcEditor,
},
props: {
comment: {
type: Object,
default() {
return {}
},
},
},
data() {
return {
disabled: true,
loading: false,
form: {
content: this.comment.content,
},
users: [],
}
},
methods: {
updateEditorContent(value) {
const sanitizedContent = value.replace(/<(?:.|\n)*?>/gm, '').trim()
this.disabled = value === this.comment.content || sanitizedContent.length < 1
this.form.content = value
},
closeEditWindow() {
this.$emit('showEditCommentMenu', false)
},
handleSubmit() {
this.loading = true
this.disabled = true
this.$apollo
.mutate({
mutation: CommentMutations(this.$i18n).UpdateComment,
variables: {
content: this.form.content,
id: this.comment.id,
},
})
.then(() => {
this.loading = false
this.$toast.success(this.$t('post.comment.updated'))
this.disabled = false
this.$emit('showEditCommentMenu', false)
})
.catch(err => {
this.$toast.error(err.message)
})
},
},
apollo: {
User: {
query() {
return gql`
{
User(orderBy: slug_asc) {
id
slug
}
}
`
},
result({ data: { User } }) {
this.users = User
},
},
},
}
</script>

View File

@ -0,0 +1 @@
export const COMMENT_MIN_LENGTH = 1

View File

@ -77,6 +77,19 @@ export default i18n => {
`
}
export const minimisedUserQuery = () => {
return gql`
query {
User(orderBy: slug_asc) {
id
slug
name
avatar
}
}
`
}
export const notificationQuery = i18n => {
const lang = i18n.locale().toUpperCase()
return gql`

View File

@ -68,7 +68,7 @@
<ds-section slot="footer">
<hc-comment-list :post="post" />
<ds-space margin-bottom="large" />
<hc-comment-form :post="post" />
<hc-comment-form :post="post" @createComment="createComment" />
</ds-section>
</ds-card>
</transition>
@ -151,6 +151,9 @@ export default {
this.$toast.error(err.message)
}
},
async createComment(comment) {
this.post.comments.push(comment)
},
},
apollo: {
Post: {

View File

@ -83,10 +83,13 @@ export default ({ app = {} }) => {
return excerpt
},
removeHtml: content => {
removeHtml: (content, replaceLinebreaks = true) => {
if (!content) return ''
// replace linebreaks with spaces first
let contentExcerpt = content.replace(/<br>/gim, ' ').trim()
let contentExcerpt = content
if (replaceLinebreaks) {
// replace linebreaks with spaces first
contentExcerpt = contentExcerpt.replace(/<br>/gim, ' ').trim()
}
// remove the rest of the HTML
contentExcerpt = contentExcerpt.replace(/<(?:.|\n)*?>/gm, '').trim()