Ver Fonte

feat: comments delete + refresh on post + formatting

NGPixel há 5 anos atrás
pai
commit
8a74904731

+ 178 - 37
client/components/comments.vue

@@ -51,7 +51,7 @@
         v-icon(left) mdi-comment
         span.text-none Post Comment
     v-divider.mt-3(v-if='permissions.write')
-    .pa-5.d-flex.align-center.justify-center(v-if='isLoading')
+    .pa-5.d-flex.align-center.justify-center(v-if='isLoading && !hasLoadedOnce')
       v-progress-circular(
         indeterminate
         size='20'
@@ -63,22 +63,38 @@
       dense
       v-else-if='comments && comments.length > 0'
       )
-      v-timeline-item(
+      v-timeline-item.comments-post(
         color='pink darken-4'
         large
         v-for='cm of comments'
         :key='`comment-` + cm.id'
+        :id='`comment-post-id-` + cm.id'
         )
         template(v-slot:icon)
-          v-avatar
-            v-img(src='http://i.pravatar.cc/64')
+          v-avatar(color='blue-grey')
+            //- v-img(src='http://i.pravatar.cc/64')
+            span.white--text.title {{cm.initials}}
         v-card.elevation-1
           v-card-text
-            .caption: strong {{cm.authorName}}
-            .overline.grey--text 3 minutes ago
-            .mt-3 {{cm.render}}
+            .comments-post-actions(v-if='permissions.manage && !isBusy')
+              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')
     .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.
+
+    v-dialog(v-model='deleteCommentDialogShown', max-width='500')
+      v-card
+        .dialog-header.is-red Confirm Delete
+        v-card-text.pt-5
+          span Are you sure you want to permanently delete this comment?
+          .caption: strong This action cannot be undone!
+        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')}}
 </template>
 
 <script>
@@ -92,10 +108,18 @@ export default {
     return {
       newcomment: '',
       isLoading: true,
-      canFetch: false,
+      hasLoadedOnce: false,
       comments: [],
       guestName: '',
-      guestEmail: ''
+      guestEmail: '',
+      commentToDelete: {},
+      deleteCommentDialogShown: false,
+      isBusy: false,
+      scrollOpts: {
+        duration: 1500,
+        offset: 0,
+        easing: 'easeInOutCubic'
+      }
     }
   },
   computed: {
@@ -107,10 +131,46 @@ export default {
   methods: {
     onIntersect (entries, observer, isIntersecting) {
       if (isIntersecting) {
-        this.isLoading = true
-        this.canFetch = true
+        this.fetch()
       }
     },
+    async fetch () {
+      this.isLoading = true
+      const results = await this.$apollo.query({
+        query: gql`
+          query ($locale: String!, $path: String!) {
+            comments {
+              list(locale: $locale, path: $path) {
+                id
+                render
+                authorName
+                createdAt
+                updatedAt
+              }
+            }
+          }
+        `,
+        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)
+        }
+        c.initials = initials
+        return c
+      })
+      this.isLoading = false
+      this.hasLoadedOnce = true
+    },
+    /**
+     * Post New Comment
+     */
     async postComment () {
       let rules = {
         comment: {
@@ -177,6 +237,7 @@ export default {
                   slug
                   message
                 }
+                id
               }
             }
           }
@@ -198,6 +259,10 @@ export default {
         })
 
         this.newcomment = ''
+        await this.fetch()
+        this.$nextTick(() => {
+          this.$vuetify.goTo(`#comment-post-id-${_.get(resp, 'data.comments.create.id', 0)}`, this.scrollOpts)
+        })
       } else {
         this.$store.commit('showNotification', {
           style: 'red',
@@ -205,41 +270,117 @@ export default {
           icon: 'alert'
         })
       }
-    }
-  },
-  apollo: {
-    comments: {
-      query: gql`
-        query ($pageId: Int!) {
-          comments {
-            list(pageId: $pageId) {
-              id
-              render
-              authorName
-              createdAt
-              updatedAt
+    },
+    async editComment (cm) {
+
+    },
+    deleteCommentConfirm (cm) {
+      this.commentToDelete = cm
+      this.deleteCommentDialogShown = true
+    },
+    /**
+     * Delete Comment
+     */
+    async deleteComment () {
+      this.$store.commit(`loadingStart`, 'comments-delete')
+      this.isBusy = true
+      this.deleteCommentDialogShown = false
+
+      const resp = await this.$apollo.mutate({
+        mutation: gql`
+          mutation (
+            $id: Int!
+          ) {
+            comments {
+              delete (
+                id: $id
+              ) {
+                responseResult {
+                  succeeded
+                  errorCode
+                  slug
+                  message
+                }
+              }
             }
           }
+        `,
+        variables: {
+          id: this.commentToDelete.id
         }
-      `,
-      variables() {
-        return {
-          pageId: this.pageId
-        }
-      },
-      skip () {
-        return !this.canFetch
-      },
-      fetchPolicy: 'cache-and-network',
-      update: (data) => data.comments.list,
-      watchLoading (isLoading) {
-        this.isLoading = isLoading
+      })
+
+      if (_.get(resp, 'data.comments.delete.responseResult.succeeded', false)) {
+        this.$store.commit('showNotification', {
+          style: 'success',
+          message: 'Comment was deleted successfully.',
+          icon: 'check'
+        })
+
+        this.comments = _.reject(this.comments, ['id', this.commentToDelete.id])
+      } else {
+        this.$store.commit('showNotification', {
+          style: 'red',
+          message: _.get(resp, 'data.comments.delete.responseResult.message', 'An unexpected error occured.'),
+          icon: 'alert'
+        })
       }
+      this.isBusy = false
+      this.$store.commit(`loadingStop`, 'comments-delete')
     }
   }
 }
 </script>
 
 <style lang="scss">
+.comments-post {
+  position: relative;
 
+  &: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%;
+    }
+
+    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;
+    }
+  }
+}
 </style>

+ 1 - 1
client/themes/default/scss/app.scss

@@ -823,7 +823,7 @@
     border-radius: 7px 7px 0 0;
 
     @at-root .theme--dark & {
-      background-color: lighten(mc('grey', '900'), 5%);
+      background-color: lighten(mc('blue-grey', '900'), 5%);
     }
   }
 

+ 43 - 6
server/graph/resolvers/comment.js

@@ -40,7 +40,25 @@ module.exports = {
      * Fetch list of comments for a page
      */
     async list (obj, args, context) {
-      return []
+      const page = await WIKI.models.pages.getPage(args)
+      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)
+          return comments.map(c => ({
+            ...c,
+            authorName: c.name,
+            authorEmail: c.email,
+            authorIP: c.ip
+          }))
+        } else {
+          throw new WIKI.Error.PageViewForbidden()
+        }
+      } else {
+        return []
+      }
     }
   },
   CommentMutation: {
@@ -49,12 +67,31 @@ module.exports = {
      */
     async create (obj, args, context) {
       try {
-        // WIKI.data.commentProvider.create({
-        //   ...args,
-        //   user: context.req.user
-        // })
+        const cmId = await WIKI.models.comments.postNewComment({
+          ...args,
+          user: context.req.user,
+          ip: context.req.ip
+        })
+        return {
+          responseResult: graphHelper.generateSuccess('New comment posted successfully'),
+          id: cmId
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
+    /**
+     * Delete an Existing Comment
+     */
+    async delete (obj, args, context) {
+      try {
+        await WIKI.models.comments.deleteComment({
+          id: args.id,
+          user: context.req.user,
+          ip: context.req.ip
+        })
         return {
-          responseResult: graphHelper.generateSuccess('New comment posted successfully')
+          responseResult: graphHelper.generateSuccess('Comment deleted successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)

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

@@ -18,7 +18,8 @@ type CommentQuery {
   providers: [CommentProvider] @auth(requires: ["manage:system"])
 
   list(
-    pageId: Int!
+    locale: String!
+    path: String!
   ): [CommentPost]! @auth(requires: ["read:comments", "manage:system"])
 
   single(
@@ -41,7 +42,7 @@ type CommentMutation {
     content: String!
     guestName: String
     guestEmail: String
-  ): DefaultResponse @auth(requires: ["write:comments", "manage:system"])
+  ): CommentCreateResponse @auth(requires: ["write:comments", "manage:system"])
 
   update(
     id: Int!
@@ -85,3 +86,8 @@ type CommentPost {
   createdAt: Date!
   updatedAt: Date!
 }
+
+type CommentCreateResponse {
+  responseResult: ResponseStatus
+  id: Int
+}

+ 20 - 0
server/helpers/error.js

@@ -97,6 +97,26 @@ 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
+  }),
+  CommentManageForbidden: CustomError('CommentManageForbidden', {
+    message: 'You are not authorized to manage comments on this page.',
+    code: 8004
+  }),
+  CommentNotFound: CustomError('CommentNotFound', {
+    message: 'This comment does not exist.',
+    code: 8005
+  }),
   InputInvalid: CustomError('InputInvalid', {
     message: 'Input data is invalid.',
     code: 1012

+ 102 - 0
server/models/comments.js

@@ -1,4 +1,8 @@
 const Model = require('objection').Model
+const validate = require('validate.js')
+const _ = require('lodash')
+
+/* global WIKI */
 
 /**
  * Comments model
@@ -52,4 +56,102 @@ module.exports = class Comment extends Model {
     this.createdAt = new Date().toISOString()
     this.updatedAt = new Date().toISOString()
   }
+
+  /**
+   * Post New Comment
+   */
+  static async postNewComment ({ pageId, replyTo, content, guestName, guestEmail, user, ip }) {
+    // -> Input validation
+    if (user.id === 2) {
+      const validation = validate({
+        email: _.toLower(guestEmail),
+        name: guestName
+      }, {
+        email: {
+          email: true,
+          length: {
+            maximum: 255
+          }
+        },
+        name: {
+          presence: {
+            allowEmpty: false
+          },
+          length: {
+            minimum: 2,
+            maximum: 255
+          }
+        }
+      }, { format: 'flat' })
+
+      if (validation && validation.length > 0) {
+        throw new WIKI.Error.InputInvalid(validation[0])
+      }
+    }
+
+    content = _.trim(content)
+    if (content.length < 2) {
+      throw new WIKI.Error.CommentContentMissing()
+    }
+
+    // -> Load Page
+    const page = await WIKI.models.pages.getPageFromDb(pageId)
+    if (page) {
+      if (!WIKI.auth.checkAccess(user, ['write:comments'], {
+        path: page.path,
+        locale: page.localeCode
+      })) {
+        throw new WIKI.Error.CommentPostForbidden()
+      }
+    } else {
+      throw new WIKI.Error.PageNotFound()
+    }
+
+    // -> Process by comment provider
+    return WIKI.data.commentProvider.create({
+      page,
+      replyTo,
+      content,
+      user: {
+        ...user,
+        ...(user.id === 2) ? {
+          name: guestName,
+          email: guestEmail
+        } : {},
+        ip
+      }
+    })
+  }
+
+  /**
+   * Delete an Existing Comment
+   */
+  static async deleteComment ({ id, 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
+    await WIKI.data.commentProvider.remove({
+      id,
+      page,
+      user: {
+        ...user,
+        ip
+      }
+    })
+  }
 }

+ 23 - 6
server/modules/comments/default/comment.js

@@ -74,7 +74,7 @@ module.exports = {
       ip: user.ip
     }
 
-    // Check for Spam with Akismet
+    // -> Check for Spam with Akismet
     if (akismetClient) {
       let userRole = 'user'
       if (user.groups.indexOf(1) >= 0) {
@@ -106,16 +106,33 @@ module.exports = {
       }
     }
 
-    // Save Comment
-    await WIKI.models.comments.query().insert(newComment)
+    // -> Save Comment to DB
+    const cm = await WIKI.models.comments.query().insert(newComment)
+
+    // -> Return Comment ID
+    return cm.id
   },
   async update ({ id, content, user, ip }) {
 
   },
+  /**
+   * Delete an existing comment by ID
+   */
   async remove ({ id, user, ip }) {
-
+    return WIKI.models.comments.query().findById(id).delete()
   },
-  async count ({ pageId }) {
-
+  /**
+   * Get the page ID from a comment ID
+   */
+  async getPageIdFromCommentId (id) {
+    const result = await WIKI.models.comments.query().select('pageId').findById(id)
+    return (result) ? result.pageId : false
+  },
+  /**
+   * Get the total comments count for a page ID
+   */
+  async count (pageId) {
+    const result = await WIKI.models.comments.query().count('* as total').where('pageId', pageId).first()
+    return _.toSafeInteger(result.total)
   }
 }