Răsfoiți Sursa

feat: admin comments page

NGPixel 5 ani în urmă
părinte
comite
1def5289af

+ 3 - 2
client/components/admin.vue

@@ -62,8 +62,8 @@
             v-list-item(to='/auth', color='primary')
             v-list-item(to='/auth', color='primary')
               v-list-item-avatar(size='24', tile): v-icon mdi-lock-outline
               v-list-item-avatar(size='24', tile): v-icon mdi-lock-outline
               v-list-item-title {{ $t('admin:auth.title') }}
               v-list-item-title {{ $t('admin:auth.title') }}
-            v-list-item(to='/comments', disabled)
-              v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-comment-text-outline
+            v-list-item(to='/comments')
+              v-list-item-avatar(size='24', tile): v-icon mdi-comment-text-outline
               v-list-item-title {{ $t('admin:comments.title') }}
               v-list-item-title {{ $t('admin:comments.title') }}
             v-list-item(to='/editor', disabled)
             v-list-item(to='/editor', disabled)
               v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-playlist-edit
               v-list-item-avatar(size='24', tile): v-icon(color='grey lighten-2') mdi-playlist-edit
@@ -171,6 +171,7 @@ const router = new VueRouter({
     { path: '/users/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users-edit.vue') },
     { path: '/users/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users-edit.vue') },
     { path: '/analytics', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-analytics.vue') },
     { path: '/analytics', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-analytics.vue') },
     { path: '/auth', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-auth.vue') },
     { path: '/auth', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-auth.vue') },
+    { path: '/comments', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-comments.vue') },
     { path: '/rendering', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-rendering.vue') },
     { path: '/rendering', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-rendering.vue') },
     { path: '/editor', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-editor.vue') },
     { path: '/editor', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-editor.vue') },
     { path: '/extensions', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-extensions.vue') },
     { path: '/extensions', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-extensions.vue') },

+ 205 - 0
client/components/admin/admin-comments.vue

@@ -0,0 +1,205 @@
+<template lang='pug'>
+  v-container(fluid, grid-list-lg)
+    v-layout(row, wrap)
+      v-flex(xs12)
+        .admin-header
+          img.animated.fadeInUp(src='/svg/icon-chat-bubble.svg', alt='Comments', style='width: 80px;')
+          .admin-header-title
+            .headline.primary--text.animated.fadeInLeft {{$t('admin:comments.title')}}
+            .subtitle-1.grey--text.animated.fadeInLeft.wait-p2s {{$t('admin:comments.subtitle')}}
+          v-spacer
+          v-btn.mx-1.animated.fadeInDown.wait-p2s(outlined, color='grey', @click='refresh', large)
+            v-icon mdi-refresh
+          v-btn.ml-1.animated.fadeInDown(color='success', @click='save', depressed, large)
+            v-icon(left) mdi-check
+            span {{$t('common:actions.apply')}}
+
+      v-flex(lg3, xs12)
+        v-card.animated.fadeInUp
+          v-toolbar(flat, color='primary', dark, dense)
+            .subtitle-1 {{$t('admin:comments.providers')}}
+          v-list.py-0(two-line, dense)
+            template(v-for='(provider, idx) in providers')
+              v-list-item(:key='provider.key', @click='selectedProvider = provider.key', :disabled='!provider.isAvailable')
+                v-list-item-avatar(size='24')
+                  v-icon(color='grey', v-if='!provider.isAvailable') mdi-minus-box-outline
+                  v-icon(color='primary', v-else-if='provider.key === selectedProvider') mdi-checkbox-marked-circle-outline
+                  v-icon(color='grey', v-else) mdi-checkbox-blank-circle-outline
+                v-list-item-content
+                  v-list-item-title.body-2(:class='!provider.isAvailable ? `grey--text` : (selectedProvider === provider.key ? `primary--text` : ``)') {{ provider.title }}
+                  v-list-item-subtitle: .caption(:class='!provider.isAvailable ? `grey--text text--lighten-1` : (selectedProvider === provider.key ? `blue--text ` : ``)') {{ provider.description }}
+                v-list-item-avatar(v-if='selectedProvider === provider.key', size='24')
+                  v-icon.animated.fadeInLeft(color='primary', large) mdi-chevron-right
+              v-divider(v-if='idx < providers.length - 1')
+
+      v-flex(lg9, xs12)
+        v-card.animated.fadeInUp.wait-p2s
+          v-toolbar(color='primary', dense, flat, dark)
+            .subtitle-1 {{provider.title}}
+          v-card-text
+            .providerlogo
+              img(:src='provider.logo', :alt='provider.title')
+            .caption.pt-3 {{provider.description}}
+            .caption.pb-3: a(:href='provider.website') {{provider.website}}
+            v-divider.mt-3
+            .overline.my-5 {{$t('admin:comments.providerConfig')}}
+            .body-2.ml-3(v-if='!provider.config || provider.config.length < 1'): em {{$t('admin:comments.providerNoConfig')}}
+            template(v-else, v-for='cfg in provider.config')
+              v-select(
+                v-if='cfg.value.type === "string" && cfg.value.enum'
+                outlined
+                :items='cfg.value.enum'
+                :key='cfg.key'
+                :label='cfg.value.title'
+                v-model='cfg.value.value'
+                prepend-icon='mdi-cog-box'
+                :hint='cfg.value.hint ? cfg.value.hint : ""'
+                persistent-hint
+                :class='cfg.value.hint ? "mb-2" : ""'
+              )
+              v-switch.mb-3(
+                v-else-if='cfg.value.type === "boolean"'
+                :key='cfg.key'
+                :label='cfg.value.title'
+                v-model='cfg.value.value'
+                color='primary'
+                prepend-icon='mdi-cog-box'
+                :hint='cfg.value.hint ? cfg.value.hint : ""'
+                persistent-hint
+                inset
+                )
+              v-textarea(
+                v-else-if='cfg.value.type === "string" && cfg.value.multiline'
+                outlined
+                :key='cfg.key'
+                :label='cfg.value.title'
+                v-model='cfg.value.value'
+                prepend-icon='mdi-cog-box'
+                :hint='cfg.value.hint ? cfg.value.hint : ""'
+                persistent-hint
+                :class='cfg.value.hint ? "mb-2" : ""'
+                )
+              v-text-field(
+                v-else
+                outlined
+                :key='cfg.key'
+                :label='cfg.value.title'
+                v-model='cfg.value.value'
+                prepend-icon='mdi-cog-box'
+                :hint='cfg.value.hint ? cfg.value.hint : ""'
+                persistent-hint
+                :class='cfg.value.hint ? "mb-2" : ""'
+                )
+</template>
+
+<script>
+import _ from 'lodash'
+import gql from 'graphql-tag'
+
+export default {
+  data() {
+    return {
+      providers: [],
+      selectedProvider: '',
+      provider: {}
+    }
+  },
+  watch: {
+    selectedProvider(newValue, oldValue) {
+      this.provider = _.find(this.providers, ['key', newValue]) || {}
+    },
+    providers(newValue, oldValue) {
+      this.selectedProvider = _.get(_.find(this.providers, 'isEnabled'), 'key', 'db')
+    }
+  },
+  methods: {
+    async refresh() {
+      await this.$apollo.queries.providers.refetch()
+      this.$store.commit('showNotification', {
+        message: this.$t('admin:comments.listRefreshSuccess'),
+        style: 'success',
+        icon: 'cached'
+      })
+    },
+    async save() {
+      this.$store.commit(`loadingStart`, 'admin-comments-saveproviders')
+      try {
+        const resp = await this.$apollo.mutate({
+          mutation: gql``,
+          variables: {
+            providers: this.providers.map(tgt => ({
+              isEnabled: tgt.key === this.selectedProvider,
+              key: tgt.key,
+              config: tgt.config.map(cfg => ({...cfg, value: JSON.stringify({ v: cfg.value.value })}))
+            }))
+          }
+        })
+        if (_.get(resp, 'data.comments.updateEngines.responseResult.succeeded', false)) {
+          this.$store.commit('showNotification', {
+            message: this.$t('admin:comments.configSaveSuccess'),
+            style: 'success',
+            icon: 'check'
+          })
+        } else {
+          throw new Error(_.get(resp, 'data.comments.updateEngines.responseResult.message', this.$t('common:error.unexpected')))
+        }
+      } catch (err) {
+        this.$store.commit('pushGraphError', err)
+      }
+      this.$store.commit(`loadingStop`, 'admin-comments-saveengines')
+    }
+  },
+  apollo: {
+    providers: {
+      query: gql`
+        query {
+          comments {
+            providers {
+              isEnabled
+              key
+              title
+              description
+              logo
+              website
+              isAvailable
+              config {
+                key
+                value
+              }
+            }
+          }
+        }
+      `,
+      fetchPolicy: 'network-only',
+      update: (data) => _.cloneDeep(data.comments.providers).map(str => ({
+        ...str,
+        config: _.sortBy(str.config.map(cfg => ({
+          ...cfg,
+          value: JSON.parse(cfg.value)
+        })), [t => t.value.order])
+      })),
+      watchLoading (isLoading) {
+        this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-comments-refresh')
+      }
+    }
+  }
+}
+</script>
+
+<style lang='scss' scoped>
+
+.providerlogo {
+  width: 250px;
+  height: 85px;
+  float:right;
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+
+  img {
+    max-width: 100%;
+    max-height: 50px;
+  }
+}
+
+</style>

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
client/static/svg/icon-chat-bubble.svg


+ 1 - 0
server/core/kernel.js

@@ -69,6 +69,7 @@ module.exports = {
   async postBootMaster() {
   async postBootMaster() {
     await WIKI.models.analytics.refreshProvidersFromDisk()
     await WIKI.models.analytics.refreshProvidersFromDisk()
     await WIKI.models.authentication.refreshStrategiesFromDisk()
     await WIKI.models.authentication.refreshStrategiesFromDisk()
+    await WIKI.models.commentProviders.refreshProvidersFromDisk()
     await WIKI.models.editors.refreshEditorsFromDisk()
     await WIKI.models.editors.refreshEditorsFromDisk()
     await WIKI.models.loggers.refreshLoggersFromDisk()
     await WIKI.models.loggers.refreshLoggersFromDisk()
     await WIKI.models.renderers.refreshRenderersFromDisk()
     await WIKI.models.renderers.refreshRenderersFromDisk()

+ 10 - 0
server/db/migrations-sqlite/2.3.14.js

@@ -0,0 +1,10 @@
+exports.up = knex => {
+  return knex.schema
+    .createTable('commentProviders', table => {
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config').notNullable()
+    })
+}
+
+exports.down = knex => { }

+ 16 - 0
server/db/migrations/2.3.14.js

@@ -0,0 +1,16 @@
+/* global WIKI */
+
+exports.up = knex => {
+  const dbCompat = {
+    charset: (WIKI.config.db.type === `mysql` || WIKI.config.db.type === `mariadb`)
+  }
+  return knex.schema
+    .createTable('commentProviders', table => {
+      if (dbCompat.charset) { table.charset('utf8mb4') }
+      table.string('key').notNullable().primary()
+      table.boolean('isEnabled').notNullable().defaultTo(false)
+      table.json('config').notNullable()
+    })
+}
+
+exports.down = knex => { }

+ 55 - 37
server/graph/resolvers/comment.js

@@ -1,39 +1,57 @@
+const _ = require('lodash')
+const graphHelper = require('../../helpers/graph')
+
+/* global WIKI */
+
 module.exports = {
 module.exports = {
-  // Query: {
-  //   comments(obj, args, context, info) {
-  //     return WIKI.models.Comment.findAll({ where: args })
-  //   }
-  // },
-  // Mutation: {
-  //   createComment(obj, args) {
-  //     return WIKI.models.Comment.create({
-  //       content: args.content,
-  //       author: args.userId,
-  //       document: args.documentId
-  //     })
-  //   },
-  //   deleteComment(obj, args) {
-  //     return WIKI.models.Comment.destroy({
-  //       where: {
-  //         id: args.id
-  //       },
-  //       limit: 1
-  //     })
-  //   },
-  //   modifyComment(obj, args) {
-  //     return WIKI.models.Comment.update({
-  //       content: args.content
-  //     }, {
-  //       where: { id: args.id }
-  //     })
-  //   }
-  // },
-  // Comment: {
-  //   author(cm) {
-  //     return cm.getAuthor()
-  //   },
-  //   document(cm) {
-  //     return cm.getDocument()
-  //   }
-  // }
+  Query: {
+    async comments() { return {} }
+  },
+  Mutation: {
+    async comments() { return {} }
+  },
+  CommentQuery: {
+    async providers(obj, args, context, info) {
+      const providers = await WIKI.models.commentProviders.getProviders()
+      return providers.map(provider => {
+        const providerInfo = _.find(WIKI.data.commentProviders, ['key', provider.key]) || {}
+        return {
+          ...providerInfo,
+          ...provider,
+          config: _.sortBy(_.transform(provider.config, (res, value, key) => {
+            const configData = _.get(providerInfo.props, key, false)
+            if (configData) {
+              res.push({
+                key,
+                value: JSON.stringify({
+                  ...configData,
+                  value
+                })
+              })
+            }
+          }, []), 'key')
+        }
+      })
+    }
+  },
+  CommentMutation: {
+    async updateProviders(obj, args, context) {
+      try {
+        for (let provider of args.providers) {
+          await WIKI.models.providers.query().patch({
+            isEnabled: provider.isEnabled,
+            config: _.reduce(provider.config, (result, value, key) => {
+              _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
+              return result
+            }, {})
+          }).where('key', provider.key)
+        }
+        return {
+          responseResult: graphHelper.generateSuccess('Comment Providers updated successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    }
+  }
 }
 }

+ 52 - 0
server/graph/schemas/comment.graphql

@@ -0,0 +1,52 @@
+# ===============================================
+# COMMENT
+# ===============================================
+
+extend type Query {
+  comments: CommentQuery
+}
+
+extend type Mutation {
+  comments: CommentMutation
+}
+
+# -----------------------------------------------
+# QUERIES
+# -----------------------------------------------
+
+type CommentQuery {
+  providers: [CommentProvider] @auth(requires: ["manage:system"])
+}
+
+# -----------------------------------------------
+# MUTATIONS
+# -----------------------------------------------
+
+type CommentMutation {
+  updateProviders(
+    providers: [CommentProviderInput]
+  ): DefaultResponse @auth(requires: ["manage:system"])
+
+  rebuildIndex: DefaultResponse @auth(requires: ["manage:system"])
+}
+
+# -----------------------------------------------
+# TYPES
+# -----------------------------------------------
+
+type CommentProvider {
+  isEnabled: Boolean!
+  key: String!
+  title: String!
+  description: String
+  logo: String
+  website: String
+  isAvailable: Boolean
+  config: [KeyValuePair]
+}
+
+input CommentProviderInput {
+  isEnabled: Boolean!
+  key: String!
+  config: [KeyValuePairInput]
+}

+ 100 - 0
server/models/commentProviders.js

@@ -0,0 +1,100 @@
+const Model = require('objection').Model
+const fs = require('fs-extra')
+const path = require('path')
+const _ = require('lodash')
+const yaml = require('js-yaml')
+const commonHelper = require('../helpers/common')
+
+/* global WIKI */
+
+/**
+ * CommentProvider model
+ */
+module.exports = class CommentProvider extends Model {
+  static get tableName() { return 'commentProviders' }
+  static get idColumn() { return 'key' }
+
+  static get jsonSchema () {
+    return {
+      type: 'object',
+      required: ['key', 'isEnabled'],
+
+      properties: {
+        key: {type: 'string'},
+        isEnabled: {type: 'boolean'}
+      }
+    }
+  }
+
+  static get jsonAttributes() {
+    return ['config']
+  }
+
+  static async getProvider(key) {
+    return WIKI.models.commentProviders.query().findOne({ key })
+  }
+
+  static async getProviders(isEnabled) {
+    const providers = await WIKI.models.commentProviders.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {})
+    return _.sortBy(providers, ['key'])
+  }
+
+  static async refreshProvidersFromDisk() {
+    let trx
+    try {
+      const dbProviders = await WIKI.models.commentProviders.query()
+
+      // -> Fetch definitions from disk
+      const authDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/comments'))
+      let diskProviders = []
+      for (let dir of authDirs) {
+        const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', dir, 'definition.yml'), 'utf8')
+        diskProviders.push(yaml.safeLoad(def))
+      }
+      WIKI.data.commentProviders = diskProviders.map(engine => ({
+        ...engine,
+        props: commonHelper.parseModuleProps(engine.props)
+      }))
+
+      let newProviders = []
+      for (let engine of WIKI.data.commentProviders) {
+        if (!_.some(dbProviders, ['key', engine.key])) {
+          newProviders.push({
+            key: engine.key,
+            isEnabled: engine.key === 'default',
+            config: _.transform(engine.props, (result, value, key) => {
+              _.set(result, key, value.default)
+              return result
+            }, {})
+          })
+        } else {
+          const engineConfig = _.get(_.find(dbProviders, ['key', engine.key]), 'config', {})
+          await WIKI.models.commentProviders.query().patch({
+            config: _.transform(engine.props, (result, value, key) => {
+              if (!_.has(result, key)) {
+                _.set(result, key, value.default)
+              }
+              return result
+            }, engineConfig)
+          }).where('key', engine.key)
+        }
+      }
+      if (newProviders.length > 0) {
+        trx = await WIKI.models.Objection.transaction.start(WIKI.models.knex)
+        for (let engine of newProviders) {
+          await WIKI.models.commentProviders.query(trx).insert(engine)
+        }
+        await trx.commit()
+        WIKI.logger.info(`Loaded ${newProviders.length} new comment providers: [ OK ]`)
+      } else {
+        WIKI.logger.info(`No new comment providers found: [ SKIPPED ]`)
+      }
+    } catch (err) {
+      WIKI.logger.error(`Failed to scan or load new comment providers: [ FAILED ]`)
+      WIKI.logger.error(err)
+      if (trx) {
+        trx.rollback()
+      }
+    }
+  }
+}

+ 4 - 0
server/modules/comments/commento/code.yml

@@ -0,0 +1,4 @@
+main: |
+  <div id="commento"></div>
+bodyEnd: |
+  <script defer src="{{instanceUrl}}/js/commento.js"></script>

+ 17 - 0
server/modules/comments/commento/definition.yml

@@ -0,0 +1,17 @@
+key: commento
+title: Commento
+description: A fast, privacy-focused commenting platform.
+author: requarks.io
+logo: https://static.requarks.io/logo/commento.svg
+website: https://commento.io/
+displayMode: footer
+codeTemplate: true
+isAvailable: true
+props:
+  instanceUrl:
+    type: String
+    title: Instance URL
+    default: 'https://cdn.commento.io'
+    hint: The URL (without a trailing slash) to the Commento instance. Leave the default https://cdn.commento.io if using the cloud-hosted version.
+    order: 1
+

+ 19 - 0
server/modules/comments/default/definition.yml

@@ -0,0 +1,19 @@
+key: default
+title: Default
+description: Built-in advanced comments tool.
+author: requarks.io
+logo: https://static.requarks.io/logo/wikijs-butterfly.svg
+website: https://wiki.js.org
+displayMode: dynamic
+codeTemplate: false
+isAvailable: true
+props:
+  displayMode:
+    type: String
+    title: Display mode
+    default: 'page'
+    enum:
+      - inline
+      - page
+    hint: Whether to display the comments under the content (inline) or on a dedicated page (page).
+    order: 1

+ 15 - 0
server/modules/comments/disqus/code.yml

@@ -0,0 +1,15 @@
+main: |
+  <div id="disqus_thread"></div>
+bodyEnd: |
+  <script>
+    var disqus_config = function () {
+      this.page.url = {{pageUrl}};
+      this.page.identifier = {{pageId}};
+    };
+    (function() {
+      var d = document, s = d.createElement('script');
+      s.src = 'https://{{shortName}}.disqus.com/embed.js';
+      s.setAttribute('data-timestamp', +new Date());
+      (d.head || d.body).appendChild(s);
+    })();
+  </script>

+ 16 - 0
server/modules/comments/disqus/definition.yml

@@ -0,0 +1,16 @@
+key: disqus
+title: Disqus
+description: Disqus help publishers power online discussions with comments.
+author: requarks.io
+logo: https://static.requarks.io/logo/disqus.svg
+website: https://disqus.com/
+displayMode: footer
+codeTemplate: true
+isAvailable: true
+props:
+  accountName:
+    type: String
+    title: Shortname
+    default: ''
+    hint: Unique identifier from Disqus to identify your website
+    order: 1

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff