Pārlūkot izejas kodu

feat: reconnect links after page update

Nick 5 gadi atpakaļ
vecāks
revīzija
26842ab62b

+ 3 - 2
client/components/admin/admin-storage.vue

@@ -66,8 +66,9 @@
                     v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
                     v-list-item-subtitle.red--text.caption {{$t('admin:storage.lastSyncAttempt', { time: $options.filters.moment(tgt.lastAttempt, 'from') })}}
                   v-list-item-action
                   v-list-item-action
                     v-menu
                     v-menu
-                      v-btn(slot='activator', icon)
-                        v-icon(color='red') mdi-information
+                      template(v-slot:activator='{ on }')
+                        v-btn(icon, v-on='on')
+                          v-icon(color='red') mdi-information
                       v-card(width='450')
                       v-card(width='450')
                         v-toolbar(flat, color='red', dark, dense) {{$t('admin:storage.errorMsg')}}
                         v-toolbar(flat, color='red', dark, dense) {{$t('admin:storage.errorMsg')}}
                         v-card-text {{tgt.message}}
                         v-card-text {{tgt.message}}

+ 1 - 1
client/components/common/nav-header.vue

@@ -107,7 +107,7 @@
 
 
           //- PAGE ACTIONS
           //- PAGE ACTIONS
 
 
-          template(v-if='isAuthenticated && path')
+          template(v-if='isAuthenticated && path && mode !== `edit`')
             v-menu(offset-y, bottom, nudge-bottom='30', transition='slide-y-transition')
             v-menu(offset-y, bottom, nudge-bottom='30', transition='slide-y-transition')
               template(v-slot:activator='{ on: menu }')
               template(v-slot:activator='{ on: menu }')
                 v-tooltip(bottom)
                 v-tooltip(bottom)

+ 171 - 0
server/models/pages.js

@@ -114,6 +114,9 @@ module.exports = class Page extends Model {
     this.updatedAt = new Date().toISOString()
     this.updatedAt = new Date().toISOString()
   }
   }
 
 
+  /**
+   * Cache Schema
+   */
   static get cacheSchema() {
   static get cacheSchema() {
     return new JSBinType({
     return new JSBinType({
       id: 'uint',
       id: 'uint',
@@ -142,6 +145,8 @@ module.exports = class Page extends Model {
 
 
   /**
   /**
    * Inject page metadata into contents
    * Inject page metadata into contents
+   *
+   * @returns {string} Page Contents with Injected Metadata
    */
    */
   injectMetadata () {
   injectMetadata () {
     return pageHelper.injectPageMetadata(this)
     return pageHelper.injectPageMetadata(this)
@@ -149,6 +154,8 @@ module.exports = class Page extends Model {
 
 
   /**
   /**
    * Get the page's file extension based on content type
    * Get the page's file extension based on content type
+   *
+   * @returns {string} File Extension
    */
    */
   getFileExtension() {
   getFileExtension() {
     switch (this.contentType) {
     switch (this.contentType) {
@@ -166,6 +173,7 @@ module.exports = class Page extends Model {
    *
    *
    * @param {String} raw Raw file contents
    * @param {String} raw Raw file contents
    * @param {String} contentType Content Type
    * @param {String} contentType Content Type
+   * @returns {Object} Parsed Page Metadata with Raw Content
    */
    */
   static parseMetadata (raw, contentType) {
   static parseMetadata (raw, contentType) {
     let result
     let result
@@ -204,6 +212,12 @@ module.exports = class Page extends Model {
     }
     }
   }
   }
 
 
+  /**
+   * Create a New Page
+   *
+   * @param {Object} opts Page Properties
+   * @returns {Promise} Promise of the Page Model Instance
+   */
   static async createPage(opts) {
   static async createPage(opts) {
     const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first()
     const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first()
     if (dupCheck) {
     if (dupCheck) {
@@ -259,9 +273,22 @@ module.exports = class Page extends Model {
       })
       })
     }
     }
 
 
+    // -> Reconnect Links
+    await WIKI.models.pages.reconnectLinks({
+      locale: page.localeCode,
+      path: page.path,
+      mode: 'create'
+    })
+
     return page
     return page
   }
   }
 
 
+  /**
+   * Update an Existing Page
+   *
+   * @param {Object} opts Page Properties
+   * @returns {Promise} Promise of the Page Model Instance
+   */
   static async updatePage(opts) {
   static async updatePage(opts) {
     const ogPage = await WIKI.models.pages.query().findById(opts.id)
     const ogPage = await WIKI.models.pages.query().findById(opts.id)
     if (!ogPage) {
     if (!ogPage) {
@@ -314,6 +341,12 @@ module.exports = class Page extends Model {
     return page
     return page
   }
   }
 
 
+  /**
+   * Delete an Existing Page
+   *
+   * @param {Object} opts Page Properties
+   * @returns {Promise} Promise with no value
+   */
   static async deletePage(opts) {
   static async deletePage(opts) {
     let page
     let page
     if (_.has(opts, 'id')) {
     if (_.has(opts, 'id')) {
@@ -344,8 +377,98 @@ module.exports = class Page extends Model {
         page
         page
       })
       })
     }
     }
+
+    // -> Reconnect Links
+    await WIKI.models.pages.reconnectLinks({
+      locale: page.localeCode,
+      path: page.path,
+      mode: 'delete'
+    })
+  }
+
+  /**
+   * Reconnect links to new/updated/deleted page
+   *
+   * @param {Object} opts - Page parameters
+   * @param {string} opts.path - Page Path
+   * @param {string} opts.locale - Page Locale Code
+   * @param {string} [opts.sourcePath] - Previous Page Path (move only)
+   * @param {string} [opts.sourceLocale] - Previous Page Locale Code (move only)
+   * @param {string} opts.mode - Page Update mode (new, move, delete)
+   * @returns {Promise} Promise with no value
+   */
+  static async reconnectLinks (opts) {
+    const pageHref = `/${opts.locale}/${opts.path}`
+    let replaceArgs = {
+      from: '',
+      to: ''
+    }
+    switch (opts.mode) {
+      case 'create':
+        replaceArgs.from = `<a href="${pageHref}" class="is-internal-link is-invalid-page">`
+        replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
+        break
+      case 'move':
+        const prevPageHref = `/${opts.sourceLocale}/${opts.sourcePath}`
+        replaceArgs.from = `<a href="${prevPageHref}" class="is-internal-link is-invalid-page">`
+        replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
+        break
+      case 'delete':
+        replaceArgs.from = `<a href="${pageHref}" class="is-internal-link is-valid-page">`
+        replaceArgs.to = `<a href="${pageHref}" class="is-internal-link is-invalid-page">`
+        break
+      default:
+        return false
+    }
+
+    let affectedHashes = []
+    // -> Perform replace and return affected page hashes (POSTGRES, MSSQL only)
+    if (WIKI.config.db.type === 'postgres' || WIKI.config.db.type === 'mssql') {
+      affectedHashes = await WIKI.models.pages.query()
+        .returning('hash')
+        .patch({
+          render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])
+        })
+        .whereIn('pages.id', function () {
+          this.select('pageLinks.pageId').from('pageLinks').where({
+            'pageLinks.path': opts.path,
+            'pageLinks.localeCode': opts.locale
+          })
+        })
+        .pluck('hash')
+    } else {
+      // -> Perform replace, then query affected page hashes (MYSQL, MARIADB, SQLITE only)
+      await WIKI.models.pages.query()
+        .patch({
+          render: WIKI.models.knex.raw('REPLACE(??, ?, ?)', ['render', replaceArgs.from, replaceArgs.to])
+        })
+        .whereIn('pages.id', function () {
+          this.select('pageLinks.pageId').from('pageLinks').where({
+            'pageLinks.path': opts.path,
+            'pageLinks.localeCode': opts.locale
+          })
+        })
+      affectedHashes = await WIKI.models.pages.query()
+        .column('hash')
+        .whereIn('pages.id', function () {
+          this.select('pageLinks.pageId').from('pageLinks').where({
+            'pageLinks.path': opts.path,
+            'pageLinks.localeCode': opts.locale
+          })
+        })
+        .pluck('hash')
+    }
+    for (const hash of affectedHashes) {
+      await WIKI.models.pages.deletePageFromCache({ hash })
+    }
   }
   }
 
 
+  /**
+   * Trigger the rendering of a page
+   *
+   * @param {Object} page Page Model Instance
+   * @returns {Promise} Promise with no value
+   */
   static async renderPage(page) {
   static async renderPage(page) {
     const renderJob = await WIKI.scheduler.registerJob({
     const renderJob = await WIKI.scheduler.registerJob({
       name: 'render-page',
       name: 'render-page',
@@ -355,6 +478,12 @@ module.exports = class Page extends Model {
     return renderJob.finished
     return renderJob.finished
   }
   }
 
 
+  /**
+   * Fetch an Existing Page from Cache if possible, from DB otherwise and save render to Cache
+   *
+   * @param {Object} opts Page Properties
+   * @returns {Promise} Promise of the Page Model Instance
+   */
   static async getPage(opts) {
   static async getPage(opts) {
     // -> Get from cache first
     // -> Get from cache first
     let page = await WIKI.models.pages.getPageFromCache(opts)
     let page = await WIKI.models.pages.getPageFromCache(opts)
@@ -375,6 +504,12 @@ module.exports = class Page extends Model {
     return page
     return page
   }
   }
 
 
+  /**
+   * Fetch an Existing Page from the Database
+   *
+   * @param {Object} opts Page Properties
+   * @returns {Promise} Promise of the Page Model Instance
+   */
   static async getPageFromDb(opts) {
   static async getPageFromDb(opts) {
     const queryModeID = _.isNumber(opts)
     const queryModeID = _.isNumber(opts)
     try {
     try {
@@ -426,6 +561,12 @@ module.exports = class Page extends Model {
     }
     }
   }
   }
 
 
+  /**
+   * Save a Page Model Instance to Cache
+   *
+   * @param {Object} page Page Model Instance
+   * @returns {Promise} Promise with no value
+   */
   static async savePageToCache(page) {
   static async savePageToCache(page) {
     const cachePath = path.join(process.cwd(), `data/cache/${page.hash}.bin`)
     const cachePath = path.join(process.cwd(), `data/cache/${page.hash}.bin`)
     await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({
     await fs.outputFile(cachePath, WIKI.models.pages.cacheSchema.encode({
@@ -448,6 +589,12 @@ module.exports = class Page extends Model {
     }))
     }))
   }
   }
 
 
+  /**
+   * Fetch an Existing Page from Cache
+   *
+   * @param {Object} opts Page Properties
+   * @returns {Promise} Promise of the Page Model Instance
+   */
   static async getPageFromCache(opts) {
   static async getPageFromCache(opts) {
     const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })
     const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })
     const cachePath = path.join(process.cwd(), `data/cache/${pageHash}.bin`)
     const cachePath = path.join(process.cwd(), `data/cache/${pageHash}.bin`)
@@ -470,14 +617,32 @@ module.exports = class Page extends Model {
     }
     }
   }
   }
 
 
+  /**
+   * Delete an Existing Page from Cache
+   *
+   * @param {Object} page Page Model Instance
+   * @param {string} page.hash Hash of the Page
+   * @returns {Promise} Promise with no value
+   */
   static async deletePageFromCache(page) {
   static async deletePageFromCache(page) {
     return fs.remove(path.join(process.cwd(), `data/cache/${page.hash}.bin`))
     return fs.remove(path.join(process.cwd(), `data/cache/${page.hash}.bin`))
   }
   }
 
 
+  /**
+   * Flush the contents of the Cache
+   */
   static async flushCache() {
   static async flushCache() {
     return fs.emptyDir(path.join(process.cwd(), `data/cache`))
     return fs.emptyDir(path.join(process.cwd(), `data/cache`))
   }
   }
 
 
+  /**
+   * Migrate all pages from a source locale to the target locale
+   *
+   * @param {Object} opts Migration properties
+   * @param {string} opts.sourceLocale Source Locale Code
+   * @param {string} opts.targetLocale Target Locale Code
+   * @returns {Promise} Promise with no value
+   */
   static async migrateToLocale({ sourceLocale, targetLocale }) {
   static async migrateToLocale({ sourceLocale, targetLocale }) {
     return WIKI.models.pages.query()
     return WIKI.models.pages.query()
       .patch({
       .patch({
@@ -491,6 +656,12 @@ module.exports = class Page extends Model {
       })
       })
   }
   }
 
 
+  /**
+   * Clean raw HTML from content for use in search engines
+   *
+   * @param {string} rawHTML Raw HTML
+   * @returns {string} Cleaned Content Text
+   */
   static cleanHTML(rawHTML = '') {
   static cleanHTML(rawHTML = '') {
     return striptags(rawHTML || '')
     return striptags(rawHTML || '')
       .replace(emojiRegex(), '')
       .replace(emojiRegex(), '')