diff --git a/client/components/admin/admin-storage.vue b/client/components/admin/admin-storage.vue
index af711e1f..236f29ea 100644
--- a/client/components/admin/admin-storage.vue
+++ b/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-action
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-toolbar(flat, color='red', dark, dense) {{$t('admin:storage.errorMsg')}}
v-card-text {{tgt.message}}
diff --git a/client/components/common/nav-header.vue b/client/components/common/nav-header.vue
index e7cacf11..df3ef503 100644
--- a/client/components/common/nav-header.vue
+++ b/client/components/common/nav-header.vue
@@ -107,7 +107,7 @@
//- 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')
template(v-slot:activator='{ on: menu }')
v-tooltip(bottom)
diff --git a/server/models/pages.js b/server/models/pages.js
index 27cd8bd6..929589e7 100644
--- a/server/models/pages.js
+++ b/server/models/pages.js
@@ -114,6 +114,9 @@ module.exports = class Page extends Model {
this.updatedAt = new Date().toISOString()
}
+ /**
+ * Cache Schema
+ */
static get cacheSchema() {
return new JSBinType({
id: 'uint',
@@ -142,6 +145,8 @@ module.exports = class Page extends Model {
/**
* Inject page metadata into contents
+ *
+ * @returns {string} Page Contents with Injected Metadata
*/
injectMetadata () {
return pageHelper.injectPageMetadata(this)
@@ -149,6 +154,8 @@ module.exports = class Page extends Model {
/**
* Get the page's file extension based on content type
+ *
+ * @returns {string} File Extension
*/
getFileExtension() {
switch (this.contentType) {
@@ -166,6 +173,7 @@ module.exports = class Page extends Model {
*
* @param {String} raw Raw file contents
* @param {String} contentType Content Type
+ * @returns {Object} Parsed Page Metadata with Raw Content
*/
static parseMetadata (raw, contentType) {
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) {
const dupCheck = await WIKI.models.pages.query().select('id').where('localeCode', opts.locale).where('path', opts.path).first()
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
}
+ /**
+ * Update an Existing Page
+ *
+ * @param {Object} opts Page Properties
+ * @returns {Promise} Promise of the Page Model Instance
+ */
static async updatePage(opts) {
const ogPage = await WIKI.models.pages.query().findById(opts.id)
if (!ogPage) {
@@ -314,6 +341,12 @@ module.exports = class Page extends Model {
return page
}
+ /**
+ * Delete an Existing Page
+ *
+ * @param {Object} opts Page Properties
+ * @returns {Promise} Promise with no value
+ */
static async deletePage(opts) {
let page
if (_.has(opts, 'id')) {
@@ -344,8 +377,98 @@ module.exports = class Page extends Model {
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 = ``
+ replaceArgs.to = ``
+ break
+ case 'move':
+ const prevPageHref = `/${opts.sourceLocale}/${opts.sourcePath}`
+ replaceArgs.from = ``
+ replaceArgs.to = ``
+ break
+ case 'delete':
+ replaceArgs.from = ``
+ replaceArgs.to = ``
+ 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) {
const renderJob = await WIKI.scheduler.registerJob({
name: 'render-page',
@@ -355,6 +478,12 @@ module.exports = class Page extends Model {
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) {
// -> Get from cache first
let page = await WIKI.models.pages.getPageFromCache(opts)
@@ -375,6 +504,12 @@ module.exports = class Page extends Model {
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) {
const queryModeID = _.isNumber(opts)
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) {
const cachePath = path.join(process.cwd(), `data/cache/${page.hash}.bin`)
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) {
const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale, privateNS: opts.isPrivate ? 'TODO' : '' })
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) {
return fs.remove(path.join(process.cwd(), `data/cache/${page.hash}.bin`))
}
+ /**
+ * Flush the contents of the Cache
+ */
static async flushCache() {
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 }) {
return WIKI.models.pages.query()
.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 = '') {
return striptags(rawHTML || '')
.replace(emojiRegex(), '')