wikijs-fork/client/components/comments.vue

554 lines
16 KiB
Vue
Raw Normal View History

2020-05-17 22:38:23 +00:00
<template lang="pug">
div(v-intersect.once='onIntersect')
2020-05-17 22:38:23 +00:00
v-textarea#discussion-new(
2020-05-23 22:49:10 +00:00
outlined
2020-05-17 22:38:23 +00:00
flat
:placeholder='$t(`common:comments.newPlaceholder`)'
2020-05-17 22:38:23 +00:00
auto-grow
dense
rows='3'
hide-details
2020-05-23 22:49:10 +00:00
v-model='newcomment'
color='blue-grey darken-2'
:background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
v-if='permissions.write'
:aria-label='$t(`common:comments.fieldContent`)'
2020-05-17 22:38:23 +00:00
)
v-row.mt-2(dense, v-if='!isAuthenticated && permissions.write')
v-col(cols='12', lg='6')
v-text-field(
outlined
color='blue-grey darken-2'
:background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
:placeholder='$t(`common:comments.fieldName`)'
hide-details
dense
autocomplete='name'
v-model='guestName'
:aria-label='$t(`common:comments.fieldName`)'
)
v-col(cols='12', lg='6')
v-text-field(
outlined
color='blue-grey darken-2'
:background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
:placeholder='$t(`common:comments.fieldEmail`)'
hide-details
type='email'
dense
autocomplete='email'
v-model='guestEmail'
:aria-label='$t(`common:comments.fieldEmail`)'
)
.d-flex.align-center.pt-3(v-if='permissions.write')
2020-05-17 22:38:23 +00:00
v-icon.mr-1(color='blue-grey') mdi-language-markdown-outline
.caption.blue-grey--text {{$t('common:comments.markdownFormat')}}
2020-05-17 22:38:23 +00:00
v-spacer
.caption.mr-3(v-if='isAuthenticated')
i18next(tag='span', path='common:comments.postingAs')
2020-06-21 05:06:40 +00:00
strong(place='name') {{userDisplayName}}
2020-05-17 22:38:23 +00:00
v-btn(
dark
2020-05-23 22:49:10 +00:00
color='blue-grey darken-2'
@click='postComment'
depressed
:aria-label='$t(`common:comments.postComment`)'
2020-05-17 22:38:23 +00:00
)
v-icon(left) mdi-comment
span.text-none {{$t('common:comments.postComment')}}
v-divider.mt-3(v-if='permissions.write')
.pa-5.d-flex.align-center.justify-center(v-if='isLoading && !hasLoadedOnce')
2020-05-23 22:49:10 +00:00
v-progress-circular(
indeterminate
size='20'
width='1'
color='blue-grey'
)
.caption.blue-grey--text.pl-3: em {{$t('common:comments.loading')}}
2020-05-17 22:38:23 +00:00
v-timeline(
dense
2020-05-23 22:49:10 +00:00
v-else-if='comments && comments.length > 0'
2020-05-17 22:38:23 +00:00
)
v-timeline-item.comments-post(
2020-05-17 22:38:23 +00:00
color='pink darken-4'
large
2020-05-23 22:49:10 +00:00
v-for='cm of comments'
:key='`comment-` + cm.id'
:id='`comment-post-id-` + cm.id'
2020-05-17 22:38:23 +00:00
)
template(v-slot:icon)
v-avatar(color='blue-grey')
//- v-img(src='http://i.pravatar.cc/64')
span.white--text.title {{cm.initials}}
2020-05-17 22:38:23 +00:00
v-card.elevation-1
v-card-text
2020-05-31 22:15:15 +00:00
.comments-post-actions(v-if='permissions.manage && !isBusy && commentEditId === 0')
v-icon.mr-3(small, @click='editComment(cm)') mdi-pencil
v-icon(small, @click='deleteCommentConfirm(cm)') mdi-delete
.comments-post-name.caption: strong {{cm.authorName}}
.comments-post-date.overline.grey--text {{cm.createdAt | moment('from') }} #[em(v-if='cm.createdAt !== cm.updatedAt') - {{$t('common:comments.modified', { reldate: $options.filters.moment(cm.updatedAt, 'from') })}}]
2020-05-31 22:15:15 +00:00
.comments-post-content.mt-3(v-if='commentEditId !== cm.id', v-html='cm.render')
.comments-post-editcontent.mt-3(v-else)
v-textarea(
outlined
flat
auto-grow
dense
rows='3'
hide-details
v-model='commentEditContent'
color='blue-grey darken-2'
:background-color='$vuetify.theme.dark ? `grey darken-5` : `white`'
)
.d-flex.align-center.pt-3
v-spacer
v-btn.mr-3(
dark
color='blue-grey darken-2'
@click='editCommentCancel'
outlined
)
v-icon(left) mdi-close
span.text-none {{$t('common:action.cancel')}}
2020-05-31 22:15:15 +00:00
v-btn(
dark
color='blue-grey darken-2'
@click='updateComment'
depressed
)
v-icon(left) mdi-comment
span.text-none {{$t('common:comments.updateComment')}}
.pt-5.text-center.body-2.blue-grey--text(v-else-if='permissions.write') {{$t('common:comments.beFirst')}}
.text-center.body-2.blue-grey--text(v-else) {{$t('common:comments.none')}}
v-dialog(v-model='deleteCommentDialogShown', max-width='500')
v-card
.dialog-header.is-red {{$t('common:comments.deleteConfirmTitle')}}
v-card-text.pt-5
span {{$t('common:comments.deleteWarn')}}
.caption: strong {{$t('common:comments.deletePermanentWarn')}}
v-card-chin
v-spacer
v-btn(text, @click='deleteCommentDialogShown = false') {{$t('common:actions.cancel')}}
v-btn(color='red', dark, @click='deleteComment') {{$t('common:actions.delete')}}
2020-05-17 22:38:23 +00:00
</template>
<script>
2020-05-23 22:49:10 +00:00
import gql from 'graphql-tag'
import { get } from 'vuex-pathify'
import validate from 'validate.js'
import _ from 'lodash'
2020-05-23 22:49:10 +00:00
2020-05-17 22:38:23 +00:00
export default {
2020-05-23 22:49:10 +00:00
data () {
return {
newcomment: '',
isLoading: true,
hasLoadedOnce: false,
comments: [],
guestName: '',
guestEmail: '',
commentToDelete: {},
2020-05-31 22:15:15 +00:00
commentEditId: 0,
commentEditContent: null,
deleteCommentDialogShown: false,
isBusy: false,
scrollOpts: {
duration: 1500,
offset: 0,
easing: 'easeInOutCubic'
}
2020-05-23 22:49:10 +00:00
}
},
computed: {
pageId: get('page/id'),
permissions: get('page/effectivePermissions@comments'),
isAuthenticated: get('user/authenticated'),
userDisplayName: get('user/name')
2020-05-23 22:49:10 +00:00
},
methods: {
onIntersect (entries, observer, isIntersecting) {
if (isIntersecting) {
2020-05-31 19:54:20 +00:00
this.fetch(true)
}
2020-05-23 22:49:10 +00:00
},
2020-05-31 19:54:20 +00:00
async fetch (silent = false) {
this.isLoading = true
2020-05-31 19:54:20 +00:00
try {
const results = await this.$apollo.query({
query: gql`
query ($locale: String!, $path: String!) {
comments {
list(locale: $locale, path: $path) {
id
render
authorName
createdAt
updatedAt
}
}
}
2020-05-31 19:54:20 +00:00
`,
variables: {
locale: this.$store.get('page/locale'),
path: this.$store.get('page/path')
},
fetchPolicy: 'network-only'
})
this.comments = _.get(results, 'data.comments.list', []).map(c => {
const nameParts = c.authorName.toUpperCase().split(' ')
let initials = _.head(nameParts).charAt(0)
if (nameParts.length > 1) {
initials += _.last(nameParts).charAt(0)
}
2020-05-31 19:54:20 +00:00
c.initials = initials
return c
})
} catch (err) {
console.warn(err)
if (!silent) {
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'alert'
})
}
2020-05-31 19:54:20 +00:00
}
this.isLoading = false
this.hasLoadedOnce = true
},
/**
* Post New Comment
*/
2020-05-23 22:49:10 +00:00
async postComment () {
let rules = {
comment: {
presence: {
allowEmpty: false
},
length: {
minimum: 2
}
}
}
if (!this.isAuthenticated && this.permissions.write) {
rules.name = {
presence: {
allowEmpty: false
},
length: {
minimum: 2,
maximum: 255
}
}
rules.email = {
presence: {
allowEmpty: false
},
email: true
}
}
const validationResults = validate({
comment: this.newcomment,
name: this.guestName,
email: this.guestEmail
}, rules, { format: 'flat' })
if (validationResults) {
this.$store.commit('showNotification', {
style: 'red',
message: validationResults[0],
icon: 'alert'
})
return
}
2020-05-17 22:38:23 +00:00
2020-05-31 19:54:20 +00:00
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation (
$pageId: Int!
$replyTo: Int
$content: String!
$guestName: String
$guestEmail: String
) {
comments {
create (
pageId: $pageId
replyTo: $replyTo
content: $content
guestName: $guestName
guestEmail: $guestEmail
) {
responseResult {
succeeded
errorCode
slug
message
}
id
}
}
}
2020-05-31 19:54:20 +00:00
`,
variables: {
pageId: this.pageId,
replyTo: 0,
content: this.newcomment,
guestName: this.guestName,
guestEmail: this.guestEmail
}
})
2020-05-31 19:54:20 +00:00
if (_.get(resp, 'data.comments.create.responseResult.succeeded', false)) {
this.$store.commit('showNotification', {
style: 'success',
message: this.$t('common:comments.postSuccess'),
2020-05-31 19:54:20 +00:00
icon: 'check'
})
this.newcomment = ''
await this.fetch()
this.$nextTick(() => {
this.$vuetify.goTo(`#comment-post-id-${_.get(resp, 'data.comments.create.id', 0)}`, this.scrollOpts)
})
} else {
throw new Error(_.get(resp, 'data.comments.create.responseResult.message', 'An unexpected error occured.'))
}
} catch (err) {
this.$store.commit('showNotification', {
style: 'red',
2020-05-31 19:54:20 +00:00
message: err.message,
icon: 'alert'
})
}
},
2020-05-31 22:15:15 +00:00
/**
* Show Comment Editing Form
*/
async editComment (cm) {
2020-05-31 22:15:15 +00:00
this.$store.commit(`loadingStart`, 'comments-edit')
this.isBusy = true
try {
const results = await this.$apollo.query({
query: gql`
query ($id: Int!) {
comments {
single(id: $id) {
content
}
}
}
`,
variables: {
id: cm.id
},
fetchPolicy: 'network-only'
})
this.commentEditContent = _.get(results, 'data.comments.single.content', null)
if (this.commentEditContent === null) {
throw new Error('Failed to load comment content.')
}
} catch (err) {
console.warn(err)
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'alert'
})
}
this.commentEditId = cm.id
this.isBusy = false
this.$store.commit(`loadingStop`, 'comments-edit')
},
/**
* Cancel Comment Edit
*/
editCommentCancel () {
this.commentEditId = 0
this.commentEditContent = null
},
/**
* Update Comment with new content
*/
async updateComment () {
this.$store.commit(`loadingStart`, 'comments-edit')
this.isBusy = true
try {
if (this.commentEditContent.length < 2) {
throw new Error(this.$t('common:comments.contentMissingError'))
2020-05-31 22:15:15 +00:00
}
const resp = await this.$apollo.mutate({
mutation: gql`
mutation (
$id: Int!
$content: String!
) {
comments {
update (
id: $id,
content: $content
) {
responseResult {
succeeded
errorCode
slug
message
}
render
}
}
}
`,
variables: {
id: this.commentEditId,
content: this.commentEditContent
}
})
2020-05-31 22:15:15 +00:00
if (_.get(resp, 'data.comments.update.responseResult.succeeded', false)) {
this.$store.commit('showNotification', {
style: 'success',
message: this.$t('common:comments.updateSuccess'),
2020-05-31 22:15:15 +00:00
icon: 'check'
})
const cm = _.find(this.comments, ['id', this.commentEditId])
cm.render = _.get(resp, 'data.comments.update.render', '-- Failed to load updated comment --')
cm.updatedAt = (new Date()).toISOString()
this.editCommentCancel()
} else {
throw new Error(_.get(resp, 'data.comments.delete.responseResult.message', 'An unexpected error occured.'))
}
} catch (err) {
console.warn(err)
this.$store.commit('showNotification', {
style: 'red',
message: err.message,
icon: 'alert'
})
}
this.isBusy = false
this.$store.commit(`loadingStop`, 'comments-edit')
},
2020-05-31 22:15:15 +00:00
/**
* Show Delete Comment Confirmation Dialog
*/
deleteCommentConfirm (cm) {
this.commentToDelete = cm
this.deleteCommentDialogShown = true
},
/**
* Delete Comment
*/
async deleteComment () {
this.$store.commit(`loadingStart`, 'comments-delete')
this.isBusy = true
this.deleteCommentDialogShown = false
2020-05-31 19:54:20 +00:00
try {
const resp = await this.$apollo.mutate({
mutation: gql`
mutation (
$id: Int!
) {
comments {
delete (
id: $id
) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
2020-05-23 22:49:10 +00:00
}
2020-05-31 19:54:20 +00:00
`,
variables: {
id: this.commentToDelete.id
2020-05-23 22:49:10 +00:00
}
})
2020-05-31 19:54:20 +00:00
if (_.get(resp, 'data.comments.delete.responseResult.succeeded', false)) {
this.$store.commit('showNotification', {
style: 'success',
message: this.$t('common:comments.deleteSuccess'),
2020-05-31 19:54:20 +00:00
icon: 'check'
})
this.comments = _.reject(this.comments, ['id', this.commentToDelete.id])
} else {
throw new Error(_.get(resp, 'data.comments.delete.responseResult.message', 'An unexpected error occured.'))
}
} catch (err) {
this.$store.commit('showNotification', {
style: 'red',
2020-05-31 19:54:20 +00:00
message: err.message,
icon: 'alert'
})
2020-05-23 22:49:10 +00:00
}
this.isBusy = false
this.$store.commit(`loadingStop`, 'comments-delete')
2020-05-23 22:49:10 +00:00
}
}
2020-05-17 22:38:23 +00:00
}
</script>
<style lang="scss">
.comments-post {
position: relative;
2020-05-17 22:38:23 +00:00
&:hover {
.comments-post-actions {
opacity: 1;
}
}
&-actions {
position: absolute;
top: 16px;
right: 16px;
opacity: 0;
transition: opacity .4s ease;
}
&-content {
> p:first-child {
padding-top: 0;
}
p {
padding-top: 1rem;
margin-bottom: 0;
}
img {
max-width: 100%;
2020-05-31 19:54:20 +00:00
border-radius: 5px;
}
code {
background-color: rgba(mc('pink', '500'), .1);
box-shadow: none;
}
pre > code {
margin-top: 1rem;
padding: 12px;
background-color: #111;
box-shadow: none;
border-radius: 5px;
width: 100%;
color: #FFF;
font-weight: 400;
font-size: .85rem;
font-family: Roboto Mono, monospace;
}
}
}
2020-05-17 22:38:23 +00:00
</style>