浏览代码

feat: edit comment

NGPixel 5 年之前
父节点
当前提交
7a946ec0f5

+ 143 - 2
client/components/comments.vue

@@ -76,12 +76,42 @@
             span.white--text.title {{cm.initials}}
         v-card.elevation-1
           v-card-text
-            .comments-post-actions(v-if='permissions.manage && !isBusy')
+            .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') - modified {{cm.updatedAt | moment('from') }}]
-            .comments-post-content.mt-3(v-html='cm.render')
+            .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 Cancel
+                v-btn(
+                  dark
+                  color='blue-grey darken-2'
+                  @click='updateComment'
+                  depressed
+                  )
+                  v-icon(left) mdi-comment
+                  span.text-none Update Comment
     .pt-5.text-center.body-2.blue-grey--text(v-else-if='permissions.write') Be the first to comment.
     .text-center.body-2.blue-grey--text(v-else) No comments yet.
 
@@ -113,6 +143,8 @@ export default {
       guestName: '',
       guestEmail: '',
       commentToDelete: {},
+      commentEditId: 0,
+      commentEditContent: null,
       deleteCommentDialogShown: false,
       isBusy: false,
       scrollOpts: {
@@ -286,9 +318,118 @@ export default {
         })
       }
     },
+    /**
+     * Show Comment Editing Form
+     */
     async editComment (cm) {
+      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('Comment is empty or too short!')
+        }
+        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
+          }
+        })
+
+        if (_.get(resp, 'data.comments.update.responseResult.succeeded', false)) {
+          this.$store.commit('showNotification', {
+            style: 'success',
+            message: 'Comment was updated successfully.',
+            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')
     },
+    /**
+     * Show Delete Comment Confirmation Dialog
+     */
     deleteCommentConfirm (cm) {
       this.commentToDelete = cm
       this.deleteCommentDialogShown = true

+ 50 - 7
server/graph/resolvers/comment.js

@@ -40,13 +40,10 @@ module.exports = {
      * Fetch list of comments for a page
      */
     async list (obj, args, context) {
-      const page = await WIKI.models.pages.getPage(args)
+      const page = await WIKI.models.pages.query().select('id').findOne({ localeCode: args.locale, path: args.path })
       if (page) {
-        if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], {
-          path: page.path,
-          locale: page.localeCode
-        })) {
-          const comments = await WIKI.models.comments.query().where('pageId', page.id)
+        if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], args)) {
+          const comments = await WIKI.models.comments.query().where('pageId', page.id).orderBy('createdAt')
           return comments.map(c => ({
             ...c,
             authorName: c.name,
@@ -54,11 +51,39 @@ module.exports = {
             authorIP: c.ip
           }))
         } else {
-          throw new WIKI.Error.PageViewForbidden()
+          throw new WIKI.Error.CommentViewForbidden()
         }
       } else {
         return []
       }
+    },
+    /**
+     * Fetch a single comment
+     */
+    async single (obj, args, context) {
+      const cm = await WIKI.data.commentProvider.getCommentById(args.id)
+      if (!cm || !cm.pageId) {
+        throw new WIKI.Error.CommentNotFound()
+      }
+      const page = await WIKI.models.pages.query().select('localeCode', 'path').findById(cm.pageId)
+      if (page) {
+        if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], {
+          path: page.path,
+          locale: page.localeCode
+        })) {
+          return {
+            ...cm,
+            authorName: cm.name,
+            authorEmail: cm.email,
+            authorIP: cm.ip
+          }
+        } else {
+          throw new WIKI.Error.CommentViewForbidden()
+        }
+      } else {
+        WIKI.logger.warn(`Comment #${cm.id} is linked to a page #${cm.pageId} that doesn't exist! [ERROR]`)
+        throw new WIKI.Error.CommentGenericError()
+      }
     }
   },
   CommentMutation: {
@@ -80,6 +105,24 @@ module.exports = {
         return graphHelper.generateError(err)
       }
     },
+    /**
+     * Update an Existing Comment
+     */
+    async update (obj, args, context) {
+      try {
+        const cmRender = await WIKI.models.comments.updateComment({
+          ...args,
+          user: context.req.user,
+          ip: context.req.ip
+        })
+        return {
+          responseResult: graphHelper.generateSuccess('Comment updated successfully'),
+          render: cmRender
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
     /**
      * Delete an Existing Comment
      */

+ 7 - 2
server/graph/schemas/comment.graphql

@@ -47,7 +47,7 @@ type CommentMutation {
   update(
     id: Int!
     content: String!
-  ): DefaultResponse @auth(requires: ["write:comments", "manage:comments", "manage:system"])
+  ): CommentUpdateResponse @auth(requires: ["write:comments", "manage:comments", "manage:system"])
 
   delete(
     id: Int!
@@ -77,7 +77,7 @@ input CommentProviderInput {
 
 type CommentPost {
   id: Int!
-  content: String!
+  content: String! @auth(requires: ["write:comments", "manage:comments", "manage:system"])
   render: String!
   authorId: Int!
   authorName: String!
@@ -91,3 +91,8 @@ type CommentCreateResponse {
   responseResult: ResponseStatus
   id: Int
 }
+
+type CommentUpdateResponse {
+  responseResult: ResponseStatus
+  render: String
+}

+ 12 - 8
server/helpers/error.js

@@ -97,18 +97,14 @@ module.exports = {
     message: 'Too many attempts! Try again later.',
     code: 1008
   }),
-  CommentGenericError: CustomError('CommentGenericError', {
-    message: 'An unexpected error occured.',
-    code: 8001
-  }),
-  CommentPostForbidden: CustomError('CommentPostForbidden', {
-    message: 'You are not authorized to post a comment on this page.',
-    code: 8002
-  }),
   CommentContentMissing: CustomError('CommentContentMissing', {
     message: 'Comment content is missing or too short.',
     code: 8003
   }),
+  CommentGenericError: CustomError('CommentGenericError', {
+    message: 'An unexpected error occured.',
+    code: 8001
+  }),
   CommentManageForbidden: CustomError('CommentManageForbidden', {
     message: 'You are not authorized to manage comments on this page.',
     code: 8004
@@ -117,6 +113,14 @@ module.exports = {
     message: 'This comment does not exist.',
     code: 8005
   }),
+  CommentPostForbidden: CustomError('CommentPostForbidden', {
+    message: 'You are not authorized to post a comment on this page.',
+    code: 8002
+  }),
+  CommentViewForbidden: CustomError('CommentViewForbidden', {
+    message: 'You are not authorized to view comments for this page.',
+    code: 8006
+  }),
   InputInvalid: CustomError('InputInvalid', {
     message: 'Input data is invalid.',
     code: 1012

+ 33 - 0
server/models/comments.js

@@ -123,6 +123,39 @@ module.exports = class Comment extends Model {
     })
   }
 
+  /**
+   * Update an Existing Comment
+   */
+  static async updateComment ({ id, content, user, ip }) {
+    // -> Load Page
+    const pageId = await WIKI.data.commentProvider.getPageIdFromCommentId(id)
+    if (!pageId) {
+      throw new WIKI.Error.CommentNotFound()
+    }
+    const page = await WIKI.models.pages.getPageFromDb(pageId)
+    if (page) {
+      if (!WIKI.auth.checkAccess(user, ['manage:comments'], {
+        path: page.path,
+        locale: page.localeCode
+      })) {
+        throw new WIKI.Error.CommentManageForbidden()
+      }
+    } else {
+      throw new WIKI.Error.PageNotFound()
+    }
+
+    // -> Process by comment provider
+    return WIKI.data.commentProvider.update({
+      id,
+      content,
+      page,
+      user: {
+        ...user,
+        ip
+      }
+    })
+  }
+
   /**
    * Delete an Existing Comment
    */

+ 27 - 15
server/modules/comments/default/comment.js

@@ -13,6 +13,17 @@ const DOMPurify = createDOMPurify(window)
 
 let akismetClient = null
 
+const mkdown = md({
+  html: false,
+  breaks: true,
+  linkify: true,
+  highlight(str, lang) {
+    return `<pre><code class="language-${lang}">${_.escape(str)}</code></pre>`
+  }
+})
+
+mkdown.use(mdEmoji)
+
 // ------------------------------------
 // Default Comment Provider
 // ------------------------------------
@@ -51,18 +62,6 @@ module.exports = {
    * Create New Comment
    */
   async create ({ page, replyTo, content, user }) {
-    // -> Render Markdown
-    const mkdown = md({
-      html: false,
-      breaks: true,
-      linkify: true,
-      highlight(str, lang) {
-        return `<pre><code class="language-${lang}">${_.escape(str)}</code></pre>`
-      }
-    })
-
-    mkdown.use(mdEmoji)
-
     // -> Build New Comment
     const newComment = {
       content,
@@ -121,13 +120,20 @@ module.exports = {
     // -> Return Comment ID
     return cm.id
   },
-  async update ({ id, content, user, ip }) {
-
+  /**
+   * Update an existing comment
+   */
+  async update ({ id, content, user }) {
+    const renderedContent = DOMPurify.sanitize(mkdown.render(content))
+    await WIKI.models.comments.query().findById(id).patch({
+      render: renderedContent
+    })
+    return renderedContent
   },
   /**
    * Delete an existing comment by ID
    */
-  async remove ({ id, user, ip }) {
+  async remove ({ id, user }) {
     return WIKI.models.comments.query().findById(id).delete()
   },
   /**
@@ -137,6 +143,12 @@ module.exports = {
     const result = await WIKI.models.comments.query().select('pageId').findById(id)
     return (result) ? result.pageId : false
   },
+  /**
+   * Get a comment by ID
+   */
+  async getCommentById (id) {
+    return WIKI.models.comments.query().findById(id)
+  },
   /**
    * Get the total comments count for a page ID
    */