فهرست منبع

feat: save page - updated + page history

NGPixel 6 سال پیش
والد
کامیت
803d86ff63

+ 73 - 26
client/components/editor.vue

@@ -6,7 +6,9 @@
           v-icon(color='green', left) check
           span.white--text(v-if='mode === "create"') {{ $t('common:actions.create') }}
           span.white--text(v-else) {{ $t('common:actions.save') }}
-        v-btn.is-icon(outline, color='red').mx-0: v-icon(color='red') close
+        v-btn(outline, color='red').mx-0
+          v-icon(color='red', left) close
+          span.white--text {{ $t('common:actions.discard') }}
         v-btn(outline, color='blue', @click.native.stop='openModal(`properties`)', dark)
           v-icon(left) sort_by_alpha
           span.white--text {{ $t('editor:page') }}
@@ -42,6 +44,7 @@ import { get, sync } from 'vuex-pathify'
 import { AtomSpinner } from 'epic-spinners'
 
 import createPageMutation from 'gql/editor/create.gql'
+import updatePageMutation from 'gql/editor/update.gql'
 
 import editorStore from '@/store/editor'
 
@@ -90,35 +93,79 @@ export default {
     },
     async save() {
       this.showProgressDialog('saving')
-      if (this.$store.get('editor/mode') === 'create') {
-        const resp = await this.$apollo.mutate({
-          mutation: createPageMutation,
-          variables: {
-            content: this.$store.get('editor/content'),
-            description: this.$store.get('editor/description'),
-            editor: 'markdown',
-            locale: this.$store.get('editor/locale'),
-            isPrivate: false,
-            isPublished: this.$store.get('editor/isPublished'),
-            path: this.$store.get('editor/path'),
-            publishEndDate: this.$store.get('editor/publishEndDate'),
-            publishStartDate: this.$store.get('editor/publishStartDate'),
-            tags: this.$store.get('editor/tags'),
-            title: this.$store.get('editor/title')
-          }
-        })
-        if (_.get(resp, 'data.pages.create.responseResult.succeeded')) {
-          this.$store.commit('showNotification', {
-            message: this.$t('editor:save.success'),
-            style: 'success',
-            icon: 'check'
+      try {
+        if (this.$store.get('editor/mode') === 'create') {
+          // --------------------------------------------
+          // -> CREATE PAGE
+          // --------------------------------------------
+
+          let resp = await this.$apollo.mutate({
+            mutation: createPageMutation,
+            variables: {
+              content: this.$store.get('editor/content'),
+              description: this.$store.get('editor/description'),
+              editor: 'markdown',
+              locale: this.$store.get('editor/locale'),
+              isPrivate: false,
+              isPublished: this.$store.get('editor/isPublished'),
+              path: this.$store.get('editor/path'),
+              publishEndDate: this.$store.get('editor/publishEndDate'),
+              publishStartDate: this.$store.get('editor/publishStartDate'),
+              tags: this.$store.get('editor/tags'),
+              title: this.$store.get('editor/title')
+            }
           })
-          this.$store.set('editor/mode', 'update')
+          resp = _.get(resp, 'data.pages.create', {})
+          if (_.get(resp, 'responseResult.succeeded')) {
+            this.$store.commit('showNotification', {
+              message: this.$t('editor:save.success'),
+              style: 'success',
+              icon: 'check'
+            })
+            this.$store.set('editor/id', _.get(resp, 'page.id'))
+            this.$store.set('editor/mode', 'update')
+          } else {
+            throw new Error(_.get(resp, 'responseResult.message'))
+          }
         } else {
+          // --------------------------------------------
+          // -> UPDATE EXISTING PAGE
+          // --------------------------------------------
 
+          let resp = await this.$apollo.mutate({
+            mutation: updatePageMutation,
+            variables: {
+              id: this.$store.get('editor/id'),
+              content: this.$store.get('editor/content'),
+              description: this.$store.get('editor/description'),
+              editor: 'markdown',
+              locale: this.$store.get('editor/locale'),
+              isPrivate: false,
+              isPublished: this.$store.get('editor/isPublished'),
+              path: this.$store.get('editor/path'),
+              publishEndDate: this.$store.get('editor/publishEndDate'),
+              publishStartDate: this.$store.get('editor/publishStartDate'),
+              tags: this.$store.get('editor/tags'),
+              title: this.$store.get('editor/title')
+            }
+          })
+          resp = _.get(resp, 'data.pages.update', {})
+          if (_.get(resp, 'responseResult.succeeded')) {
+            this.$store.commit('showNotification', {
+              message: this.$t('editor:save.success'),
+              style: 'success',
+              icon: 'check'
+            })
+          } else {
+            throw new Error(_.get(resp, 'responseResult.message'))
+          }
         }
-      } else {
-
+      } catch (err) {
+        this.$store.commit('showNotification', {
+          message: err.message,
+          style: 'error',
+          icon: 'warning'
+        })
       }
       this.hideProgressDialog()
     }

+ 14 - 3
client/components/editor/editor-modal-properties.vue

@@ -7,13 +7,24 @@
       v-icon(color='white') sort_by_alpha
       .subheading.white--text.ml-2 Page Properties
       v-spacer
-      v-btn(
+      v-btn.mx-0(
         outline
         dark
         @click.native='close'
         )
-        v-icon(left) close
-        span Close
+        v-icon(left) check
+        span {{ $t('common:actions.ok') }}
+      v-menu
+        v-btn.is-icon(
+          slot='activator'
+          outline
+          dark
+          )
+          v-icon more_horiz
+        v-list
+          v-list-tile
+            v-list-tile-avatar: v-icon delete
+            v-list-tile-title Delete Page
     v-card(tile)
       v-card-text
         v-subheader.pl-0 Page Info

+ 23 - 16
client/components/setup.vue

@@ -50,7 +50,7 @@
               .body-1.pt-3
                 svg.icons.is-18.is-outlined.has-right-pad.is-text: use(xlink:href='#nc-cd-reader')
                 span You are about to install Wiki.js #[strong {{wikiVersion}}].
-              v-divider
+              v-divider.mt-3
               v-form
                 v-checkbox(
                   color='primary',
@@ -67,7 +67,7 @@
                   hint='Check this box if you are upgrading from Wiki.js 1.x and wish to migrate your existing data.'
                 )
             v-divider
-            .text-xs-center
+            .pt-3.text-xs-center
               v-btn(color='primary', @click='proceedToSyscheck', :disabled='loading') Start
 
           //- ==============================================
@@ -94,7 +94,7 @@
                       v-list-tile-title {{rs.title}}
                       v-list-tile-sub-title {{rs.subtitle}}
             v-divider
-            .text-xs-center
+            .pt-3.text-xs-center
               v-btn(@click='proceedToWelcome', :disabled='loading') Back
               v-btn(color='primary', @click='proceedToSyscheck', v-if='!loading && !syscheck.ok') Check Again
               v-btn(color='red', dark, @click='proceedToGeneral', v-if='!loading && !syscheck.ok') Continue Anyway
@@ -113,6 +113,8 @@
                 v-layout(row, wrap)
                   v-flex(xs12, sm6).pr-3
                     v-text-field(
+                      outline
+                      background-color='grey lighten-2'
                       v-model='conf.title',
                       label='Site Title',
                       :counter='255',
@@ -126,6 +128,8 @@
                     )
                   v-flex.pr-3(xs12, sm6)
                     v-text-field(
+                      outline
+                      background-color='grey lighten-2'
                       v-model='conf.port',
                       label='Server Port',
                       persistent-hint,
@@ -139,6 +143,8 @@
                 v-layout(row, wrap).mt-3
                   v-flex(xs12, sm6).pr-3
                     v-text-field(
+                      outline
+                      background-color='grey lighten-2'
                       v-model='conf.pathContent',
                       label='Content Data Path',
                       persistent-hint,
@@ -151,6 +157,8 @@
                     )
                   v-flex(xs12, sm6)
                     v-text-field(
+                      outline
+                      background-color='grey lighten-2'
                       v-model='conf.pathData',
                       label='Temporary Data Path',
                       persistent-hint,
@@ -170,15 +178,8 @@
                       persistent-hint,
                       hint='Should the site be accessible (read only) without login.'
                     )
-                    v-checkbox.mt-2(
-                      color='primary',
-                      v-model='conf.selfRegister',
-                      label='Allow Self-Registration',
-                      persistent-hint,
-                      hint='Can users create their own account to gain access?'
-                    )
             v-divider
-            .text-xs-center
+            .pt-3.text-xs-center
               v-btn(@click='proceedToSyscheck', :disabled='loading') Back
               v-btn(color='primary', @click='proceedToAdmin', :disabled='loading') Continue
 
@@ -196,7 +197,8 @@
                 v-layout(row, wrap)
                   v-flex(xs12)
                     v-text-field(
-                      autofocus
+                      outline
+                      background-color='grey lighten-2'
                       v-model='conf.adminEmail',
                       label='Administrator Email',
                       hint='The email address of the administrator account',
@@ -208,6 +210,8 @@
                     )
                   v-flex.pr-3(xs6)
                     v-text-field(
+                      outline
+                      background-color='grey lighten-2'
                       ref='adminPassword',
                       counter='255'
                       v-model='conf.adminPassword',
@@ -224,6 +228,8 @@
                     )
                   v-flex(xs6)
                     v-text-field(
+                      outline
+                      background-color='grey lighten-2'
                       ref='adminPasswordConfirm',
                       counter='255'
                       v-model='conf.adminPasswordConfirm',
@@ -238,7 +244,7 @@
                       data-vv-scope='admin',
                       :error-messages='errors.collect(`adminPasswordConfirm`)'
                     )
-              .text-xs-center
+              .pt-3.text-xs-center
                 v-btn(@click='proceedToGeneral', :disabled='loading') Back
                 v-btn(color='primary', @click='proceedToUpgrade', :disabled='loading') Continue
 
@@ -256,6 +262,8 @@
                 v-layout(row)
                   v-flex(xs12)
                     v-text-field(
+                      outline
+                      background-color='grey lighten-2'
                       v-model='conf.upgMongo',
                       placeholder='mongodb://',
                       label='Connection String to Wiki.js 1.x MongoDB database',
@@ -267,7 +275,7 @@
                       data-vv-scope='upgrade',
                       :error-messages='errors.collect(`upgMongo`)'
                     )
-            .text-xs-center
+            .pt-3.text-xs-center
               v-btn(@click='proceedToAdmin', :disabled='loading') Back
               v-btn(color='primary', @click='proceedToFinal', :disabled='loading') Continue
 
@@ -290,7 +298,7 @@
               v-alert(type='success', outline, :value='!loading && final.ok') Wiki.js was configured successfully and is now ready for use.
               v-alert(type='error', outline, :value='!loading && !final.ok') {{ final.error }}
             v-divider
-            .text-xs-center
+            .pt-3.text-xs-center
               v-btn(@click='!conf.upgrade ? proceedToAdmin() : proceedToUpgrade()', :disabled='loading') Back
               v-btn(color='primary', @click='proceedToFinal', v-if='!loading && !final.ok') Try Again
               v-btn(color='success', @click='finish', v-if='loading || final.ok', :disabled='loading') Continue
@@ -342,7 +350,6 @@ export default {
         pathContent: './content',
         port: siteConfig.port || 80,
         public: (siteConfig.public === true),
-        selfRegister: (siteConfig.selfRegister === true),
         telemetry: true,
         title: siteConfig.title || 'Wiki',
         upgrade: false,

+ 12 - 0
client/graph/editor/update.gql

@@ -0,0 +1,12 @@
+mutation ($id: Int!, $content: String, $description: String, $editor: String, $isPrivate: Boolean, $isPublished: Boolean, $locale: String, $path: String, $publishEndDate: Date, $publishStartDate: Date, $tags: [String], $title: String) {
+  pages {
+    update(id: $id, content: $content, description: $description, editor: $editor, isPrivate: $isPrivate, isPublished: $isPublished, locale: $locale, path: $path, publishEndDate: $publishEndDate, publishStartDate: $publishStartDate, tags: $tags, title: $title) {
+      responseResult {
+        succeeded
+        errorCode
+        slug
+        message
+      }
+    }
+  }
+}

+ 1 - 0
client/store/editor.js

@@ -1,6 +1,7 @@
 import { make } from 'vuex-pathify'
 
 const state = {
+  id: 0,
   content: '',
   description: '',
   isPublished: true,

+ 26 - 0
server/db/migrations/2.0.0.js

@@ -68,6 +68,19 @@ exports.up = knex => {
       table.string('createdAt').notNullable()
       table.string('updatedAt').notNullable()
     })
+    // PAGE HISTORY ------------------------
+    .createTable('pageHistory', table => {
+      table.increments('id').primary()
+      table.string('path').notNullable()
+      table.string('title').notNullable()
+      table.string('description')
+      table.boolean('isPrivate').notNullable().defaultTo(false)
+      table.boolean('isPublished').notNullable().defaultTo(false)
+      table.string('publishStartDate')
+      table.string('publishEndDate')
+      table.text('content')
+      table.string('createdAt').notNullable()
+    })
     // PAGES -------------------------------
     .createTable('pages', table => {
       table.increments('id').primary()
@@ -126,6 +139,12 @@ exports.up = knex => {
     // =====================================
     // RELATION TABLES
     // =====================================
+    // PAGE HISTORY TAGS ---------------------------
+    .createTable('pageHistoryTags', table => {
+      table.increments('id').primary()
+      table.integer('pageId').unsigned().references('id').inTable('pageHistory').onDelete('CASCADE')
+      table.integer('tagId').unsigned().references('id').inTable('tags').onDelete('CASCADE')
+    })
     // PAGE TAGS ---------------------------
     .createTable('pageTags', table => {
       table.increments('id').primary()
@@ -149,10 +168,17 @@ exports.up = knex => {
       table.integer('pageId').unsigned().references('id').inTable('pages')
       table.integer('authorId').unsigned().references('id').inTable('users')
     })
+    .table('pageHistory', table => {
+      table.integer('pageId').unsigned().references('id').inTable('pages')
+      table.string('editorKey').references('key').inTable('editors')
+      table.string('localeCode', 2).references('code').inTable('locales')
+      table.integer('authorId').unsigned().references('id').inTable('users')
+    })
     .table('pages', table => {
       table.string('editorKey').references('key').inTable('editors')
       table.string('localeCode', 2).references('code').inTable('locales')
       table.integer('authorId').unsigned().references('id').inTable('users')
+      table.integer('creatorId').unsigned().references('id').inTable('users')
     })
     .table('users', table => {
       table.string('providerKey').references('key').inTable('authentication').notNullable().defaultTo('local')

+ 100 - 0
server/db/models/pageHistory.js

@@ -0,0 +1,100 @@
+const Model = require('objection').Model
+
+/* global WIKI */
+
+/**
+ * Page History model
+ */
+module.exports = class PageHistory extends Model {
+  static get tableName() { return 'pageHistory' }
+
+  static get jsonSchema () {
+    return {
+      type: 'object',
+      required: ['path', 'title'],
+
+      properties: {
+        id: {type: 'integer'},
+        path: {type: 'string'},
+        title: {type: 'string'},
+        description: {type: 'string'},
+        isPublished: {type: 'boolean'},
+        publishStartDate: {type: 'string'},
+        publishEndDate: {type: 'string'},
+        content: {type: 'string'},
+
+        createdAt: {type: 'string'}
+      }
+    }
+  }
+
+  static get relationMappings() {
+    return {
+      tags: {
+        relation: Model.ManyToManyRelation,
+        modelClass: require('./tags'),
+        join: {
+          from: 'pageHistory.id',
+          through: {
+            from: 'pageHistoryTags.pageId',
+            to: 'pageHistoryTags.tagId'
+          },
+          to: 'tags.id'
+        }
+      },
+      page: {
+        relation: Model.BelongsToOneRelation,
+        modelClass: require('./pages'),
+        join: {
+          from: 'pageHistory.pageId',
+          to: 'pages.id'
+        }
+      },
+      author: {
+        relation: Model.BelongsToOneRelation,
+        modelClass: require('./users'),
+        join: {
+          from: 'pageHistory.authorId',
+          to: 'users.id'
+        }
+      },
+      editor: {
+        relation: Model.BelongsToOneRelation,
+        modelClass: require('./editors'),
+        join: {
+          from: 'pageHistory.editorKey',
+          to: 'editors.key'
+        }
+      },
+      locale: {
+        relation: Model.BelongsToOneRelation,
+        modelClass: require('./locales'),
+        join: {
+          from: 'pageHistory.localeCode',
+          to: 'locales.code'
+        }
+      }
+    }
+  }
+
+  $beforeInsert() {
+    this.createdAt = new Date().toISOString()
+  }
+
+  static async addVersion(opts) {
+    await WIKI.db.pageHistory.query().insert({
+      pageId: opts.id,
+      authorId: opts.authorId,
+      content: opts.content,
+      description: opts.description,
+      editorKey: opts.editorKey,
+      isPrivate: opts.isPrivate,
+      isPublished: opts.isPublished,
+      localeCode: opts.localeCode,
+      path: opts.path,
+      publishEndDate: opts.publishEndDate,
+      publishStartDate: opts.publishStartDate,
+      title: opts.title
+    })
+  }
+}

+ 29 - 1
server/db/models/pages.js

@@ -51,6 +51,14 @@ module.exports = class Page extends Model {
           to: 'users.id'
         }
       },
+      creator: {
+        relation: Model.BelongsToOneRelation,
+        modelClass: require('./users'),
+        join: {
+          from: 'pages.creatorId',
+          to: 'users.id'
+        }
+      },
       editor: {
         relation: Model.BelongsToOneRelation,
         modelClass: require('./editors'),
@@ -82,6 +90,7 @@ module.exports = class Page extends Model {
     const page = await WIKI.db.pages.query().insertAndFetch({
       authorId: opts.authorId,
       content: opts.content,
+      creatorId: opts.authorId,
       description: opts.description,
       editorKey: opts.editor,
       isPrivate: opts.isPrivate,
@@ -92,7 +101,26 @@ module.exports = class Page extends Model {
       publishStartDate: opts.publishStartDate,
       title: opts.title
     })
-    await WIKI.db.storage.createPage(page)
+    await WIKI.db.storage.pageEvent({
+      event: 'created',
+      page
+    })
+    return page
+  }
+
+  static async updatePage(opts) {
+    const ogPage = await WIKI.db.pages.query().findById(opts.id)
+    if (!ogPage) {
+      throw new Error('Invalid Page Id')
+    }
+    await WIKI.db.pageHistory.addVersion(ogPage)
+    const page = await WIKI.db.pages.query().patchAndFetch({
+      title: opts.title
+    }).where('id', opts.id)
+    await WIKI.db.storage.pageEvent({
+      event: 'updated',
+      page
+    })
     return page
   }
 }

+ 2 - 2
server/db/models/storage.js

@@ -87,12 +87,12 @@ module.exports = class Storage extends Model {
     }
   }
 
-  static async createPage(page) {
+  static async pageEvent(event, page) {
     const targets = await WIKI.db.storage.query().where('isEnabled', true)
     if (targets && targets.length > 0) {
       _.forEach(targets, target => {
         WIKI.queue.job.syncStorage.add({
-          event: 'created',
+          event,
           target,
           page
         }, {

+ 7 - 4
server/graph/resolvers/page.js

@@ -24,7 +24,6 @@ module.exports = {
     async create(obj, args, context) {
       const page = await WIKI.db.pages.createPage({
         ...args,
-        isPrivate: false,
         authorId: context.req.user.id
       })
       return {
@@ -38,10 +37,14 @@ module.exports = {
         responseResult: graphHelper.generateSuccess('Page has been deleted.')
       }
     },
-    async update(obj, args) {
-      await WIKI.db.groups.query().patch({ name: args.name }).where('id', args.id)
+    async update(obj, args, context) {
+      const page = await WIKI.db.pages.updatePage({
+        ...args,
+        authorId: context.req.user.id
+      })
       return {
-        responseResult: graphHelper.generateSuccess('Page has been updated.')
+        responseResult: graphHelper.generateSuccess('Page has been updated.'),
+        page
       }
     }
   },

+ 2 - 1
server/graph/schemas/page.graphql

@@ -52,6 +52,7 @@ type PageMutation {
     content: String
     description: String
     editor: String
+    isPrivate: Boolean
     isPublished: Boolean
     locale: String
     path: String
@@ -59,7 +60,7 @@ type PageMutation {
     publishStartDate: Date
     tags: [String]
     title: String
-  ): DefaultResponse
+  ): PageResponse
 
   delete(
     id: Int!