From 7128b160dd8ff6f1fca4d4c59c595cbb74b778d7 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Fri, 28 Oct 2022 04:06:56 +0000 Subject: [PATCH] feat: rendering + new page view --- server/app/data.yml | 9 +- server/controllers/common.js | 31 +- server/core/db.js | 12 +- server/core/scheduler.js | 83 +++- server/db/migrations/3.0.0.js | 5 +- server/graph/resolvers/system.js | 52 ++ server/graph/schemas/system.graphql | 8 + server/helpers/common.js | 28 ++ server/models/editors.js | 41 -- server/models/pages.js | 45 +- server/models/renderers.js | 56 ++- .../rendering/html-asciinema/definition.yml | 2 +- .../rendering/html-blockquotes/definition.yml | 2 +- .../html-codehighlighter/definition.yml | 2 +- .../rendering/html-core/definition.yml | 2 +- .../modules/rendering/html-core/renderer.js | 13 +- .../rendering/html-diagram/definition.yml | 2 +- .../html-image-prefetch/definition.yml | 2 +- .../html-mediaplayers/definition.yml | 2 +- .../rendering/html-mermaid/definition.yml | 2 +- .../rendering/html-security/definition.yml | 2 +- .../rendering/html-tabset/definition.yml | 2 +- .../rendering/html-twemoji/definition.yml | 2 +- .../rendering/markdown-abbr/definition.yml | 2 +- .../rendering/markdown-core/definition.yml | 2 +- .../rendering/markdown-core/renderer.js | 2 +- .../rendering/markdown-emoji/definition.yml | 2 +- .../markdown-expandtabs/definition.yml | 2 +- .../markdown-footnotes/definition.yml | 2 +- .../rendering/markdown-imsize/definition.yml | 2 +- .../rendering/markdown-katex/definition.yml | 2 +- .../rendering/markdown-kroki/definition.yml | 2 +- .../rendering/markdown-mathjax/definition.yml | 2 +- .../markdown-multi-table/definition.yml | 2 +- .../markdown-plantuml/definition.yml | 2 +- .../rendering/markdown-supsub/definition.yml | 2 +- .../markdown-tasklists/definition.yml | 2 +- server/tasks/workers/render-page.js | 92 ++++ server/views/page.pug | 13 - server/worker.js | 18 +- ux/src/components/PageTags.vue | 71 +-- ux/src/components/SocialSharingMenu.vue | 193 ++++---- ux/src/i18n/locales/en.json | 6 +- ux/src/layouts/AdminLayout.vue | 10 +- ux/src/layouts/MainLayout.vue | 94 ++-- ux/src/pages/AdminScheduler.vue | 114 ++++- ux/src/pages/Index.vue | 463 ++++++++---------- ux/src/router/routes.js | 23 +- 48 files changed, 957 insertions(+), 573 deletions(-) delete mode 100644 server/models/editors.js create mode 100644 server/tasks/workers/render-page.js diff --git a/server/app/data.yml b/server/app/data.yml index 091e9fb6..cb66ecbd 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -33,7 +33,7 @@ defaults: workers: 3 pollingCheck: 5 scheduledCheck: 300 - maxRetries: 5 + maxRetries: 2 retryBackoff: 60 historyExpiration: 90000 # DB defaults @@ -83,6 +83,13 @@ defaults: search: maxHits: 100 maintainerEmail: security@requarks.io +editors: + code: + contentType: html + markdown: + contentType: markdown + wysiwyg: + contentType: html groups: defaultPermissions: - 'read:pages' diff --git a/server/controllers/common.js b/server/controllers/common.js index 08e24179..34bacd88 100644 --- a/server/controllers/common.js +++ b/server/controllers/common.js @@ -528,9 +528,9 @@ router.get('/*', async (req, res, next) => { // -> Build theme code injection const injectCode = { - css: WIKI.config.theming.injectCSS, - head: WIKI.config.theming.injectHead, - body: WIKI.config.theming.injectBody + css: '', // WIKI.config.theming.injectCSS, + head: '', // WIKI.config.theming.injectHead, + body: '' // WIKI.config.theming.injectBody } // Handle missing extra field @@ -551,12 +551,12 @@ router.get('/*', async (req, res, next) => { // -> Inject comments variables const commentTmpl = { - codeTemplate: WIKI.data.commentProvider.codeTemplate, - head: WIKI.data.commentProvider.head, - body: WIKI.data.commentProvider.body, - main: WIKI.data.commentProvider.main + codeTemplate: '', // WIKI.data.commentProvider.codeTemplate, + head: '', // WIKI.data.commentProvider.head, + body: '', // WIKI.data.commentProvider.body, + main: '' // WIKI.data.commentProvider.main } - if (WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) { + if (false && WIKI.config.features.featurePageComments && WIKI.data.commentProvider.codeTemplate) { [ { key: 'pageUrl', value: `${WIKI.config.host}/i/${page.id}` }, { key: 'pageId', value: page.id } @@ -568,13 +568,14 @@ router.get('/*', async (req, res, next) => { } // -> Render view - res.render('page', { - page, - sidebar, - injectCode, - comments: commentTmpl, - effectivePermissions - }) + res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html')) + // res.render('page', { + // page, + // sidebar, + // injectCode, + // comments: commentTmpl, + // effectivePermissions + // }) } else if (pageArgs.path === 'home') { res.redirect('/_welcome') } else { diff --git a/server/core/db.js b/server/core/db.js index ac45cfb2..0b2143a4 100644 --- a/server/core/db.js +++ b/server/core/db.js @@ -21,7 +21,7 @@ module.exports = { /** * Initialize DB */ - init() { + init(workerMode = false) { let self = this WIKI.logger.info('Checking DB configuration...') @@ -85,10 +85,14 @@ module.exports = { connection: this.config, searchPath: [WIKI.config.db.schemas.wiki], pool: { - ...WIKI.config.pool, + ...workerMode ? { min: 0, max: 1 } : WIKI.config.pool, async afterCreate(conn, done) { // -> Set Connection App Name - await conn.query(`set application_name = 'Wiki.js - ${WIKI.INSTANCE_ID}:MAIN'`) + if (workerMode) { + await conn.query(`set application_name = 'Wiki.js - ${WIKI.INSTANCE_ID}'`) + } else { + await conn.query(`set application_name = 'Wiki.js - ${WIKI.INSTANCE_ID}:MAIN'`) + } done() } }, @@ -145,7 +149,7 @@ module.exports = { // Perform init tasks - this.onReady = (async () => { + this.onReady = workerMode ? Promise.resolve() : (async () => { await initTasks.connect() await initTasks.migrateFromLegacy() await initTasks.syncSchemas() diff --git a/server/core/scheduler.js b/server/core/scheduler.js index b26a9f59..323f38b8 100644 --- a/server/core/scheduler.js +++ b/server/core/scheduler.js @@ -4,6 +4,9 @@ const autoload = require('auto-load') const path = require('node:path') const cronparser = require('cron-parser') const { DateTime } = require('luxon') +const { v4: uuid } = require('uuid') +const { createDeferred } = require('../helpers/common') +const _ = require('lodash') module.exports = { workerPool: null, @@ -12,6 +15,7 @@ module.exports = { pollingRef: null, scheduledRef: null, tasks: null, + completionPromises: [], async init () { this.maxWorkers = WIKI.config.scheduler.workers === 'auto' ? (os.cpus().length - 1) : WIKI.config.scheduler.workers if (this.maxWorkers < 1) { this.maxWorkers = 1 } @@ -38,6 +42,20 @@ module.exports = { } break } + case 'jobCompleted': { + const jobPromise = _.find(this.completionPromises, ['id', payload.id]) + if (jobPromise) { + if (payload.state === 'success') { + jobPromise.resolve() + } else { + jobPromise.reject(new Error(payload.errorMessage)) + } + setTimeout(() => { + _.remove(this.completionPromises, ['id', payload.id]) + }) + } + break + } } }) @@ -56,23 +74,52 @@ module.exports = { WIKI.logger.info('Scheduler: [ STARTED ]') }, - async addJob ({ task, payload, waitUntil, maxRetries, isScheduled = false, notify = true }) { + /** + * Add a job to the scheduler + * @param {Object} opts - Job options + * @param {string} opts.task - The task name to execute. + * @param {Object} [opts.payload={}] - An optional data object to pass to the job. + * @param {Date} [opts.waitUntil] - An optional datetime after which the task is allowed to run. + * @param {Number} [opts.maxRetries] - The number of times this job can be restarted upon failure. Uses server defaults if not provided. + * @param {Boolean} [opts.isScheduled=false] - Whether this is a scheduled job. + * @param {Boolean} [opts.notify=true] - Whether to notify all instances that a new job is available. + * @param {Boolean} [opts.promise=false] - Whether to return a promise property that resolves when the job completes. + * @returns {Promise} + */ + async addJob ({ task, payload = {}, waitUntil, maxRetries, isScheduled = false, notify = true, promise = false }) { try { - await WIKI.db.knex('jobs').insert({ - task, - useWorker: !(typeof this.tasks[task] === 'function'), - payload, - maxRetries: maxRetries ?? WIKI.config.scheduler.maxRetries, - isScheduled, - waitUntil, - createdBy: WIKI.INSTANCE_ID - }) + const jobId = uuid() + const jobDefer = createDeferred() + if (promise) { + this.completionPromises.push({ + id: jobId, + added: DateTime.utc(), + resolve: jobDefer.resolve, + reject: jobDefer.reject + }) + } + await WIKI.db.knex('jobs') + .insert({ + id: jobId, + task, + useWorker: !(typeof this.tasks[task] === 'function'), + payload, + maxRetries: maxRetries ?? WIKI.config.scheduler.maxRetries, + isScheduled, + waitUntil, + createdBy: WIKI.INSTANCE_ID + }) if (notify) { WIKI.db.listener.publish('scheduler', { source: WIKI.INSTANCE_ID, - event: 'newJob' + event: 'newJob', + id: jobId }) } + return { + id: jobId, + ...promise && { promise: jobDefer.promise } + } } catch (err) { WIKI.logger.warn(`Failed to add job to scheduler: ${err.message}`) } @@ -130,6 +177,12 @@ module.exports = { completedAt: new Date() }) WIKI.logger.info(`Completed job ${job.id}: ${job.task}`) + WIKI.db.listener.publish('scheduler', { + source: WIKI.INSTANCE_ID, + event: 'jobCompleted', + state: 'success', + id: job.id + }) } catch (err) { WIKI.logger.warn(`Failed to complete job ${job.id}: ${job.task} [ FAILED ]`) WIKI.logger.warn(err) @@ -137,9 +190,17 @@ module.exports = { await WIKI.db.knex('jobHistory').where({ id: job.id }).update({ + attempt: job.retries + 1, state: 'failed', lastErrorMessage: err.message }) + WIKI.db.listener.publish('scheduler', { + source: WIKI.INSTANCE_ID, + event: 'jobCompleted', + state: 'failed', + id: job.id, + errorMessage: err.message + }) // -> Reschedule for retry if (job.retries < job.maxRetries) { const backoffDelay = (2 ** job.retries) * WIKI.config.scheduler.retryBackoff diff --git a/server/db/migrations/3.0.0.js b/server/db/migrations/3.0.0.js index c873b02d..35c2af1a 100644 --- a/server/db/migrations/3.0.0.js +++ b/server/db/migrations/3.0.0.js @@ -243,7 +243,7 @@ exports.up = async knex => { table.uuid('id').notNullable().primary().defaultTo(knex.raw('gen_random_uuid()')) table.string('module').notNullable() table.boolean('isEnabled').notNullable().defaultTo(false) - table.jsonb('config') + table.jsonb('config').notNullable().defaultTo('{}') }) // SETTINGS ---------------------------- .createTable('settings', table => { @@ -370,9 +370,6 @@ exports.up = async knex => { table.uuid('pageId').notNullable().references('id').inTable('pages').onDelete('CASCADE') table.string('localeCode', 5).references('code').inTable('locales') }) - .table('renderers', table => { - table.uuid('siteId').notNullable().references('id').inTable('sites') - }) .table('storage', table => { table.uuid('siteId').notNullable().references('id').inTable('sites') }) diff --git a/server/graph/resolvers/system.js b/server/graph/resolvers/system.js index 6b5fbed8..36d777b3 100644 --- a/server/graph/resolvers/system.js +++ b/server/graph/resolvers/system.js @@ -79,6 +79,25 @@ module.exports = { } }, Mutation: { + async cancelJob (obj, args, context) { + WIKI.logger.info(`Admin requested cancelling job ${args.id}...`) + try { + const result = await WIKI.db.knex('jobs') + .where('id', args.id) + .del() + if (result === 1) { + WIKI.logger.info(`Cancelled job ${args.id} [ OK ]`) + } else { + throw new Error('Job has already entered active state or does not exist.') + } + return { + operation: graphHelper.generateSuccess('Cancelled job successfully.') + } + } catch (err) { + WIKI.logger.warn(err) + return graphHelper.generateError(err) + } + }, async disconnectWS (obj, args, context) { WIKI.servers.ws.disconnectSockets(true) WIKI.logger.info('All active websocket connections have been terminated.') @@ -97,6 +116,39 @@ module.exports = { return graphHelper.generateError(err) } }, + async retryJob (obj, args, context) { + WIKI.logger.info(`Admin requested rescheduling of job ${args.id}...`) + try { + const job = await WIKI.db.knex('jobHistory') + .where('id', args.id) + .first() + if (!job) { + throw new Error('No such job found.') + } else if (job.state === 'interrupted') { + throw new Error('Cannot reschedule a task that has been interrupted. It will automatically be retried shortly.') + } else if (job.state === 'failed' && job.attempt < job.maxRetries) { + throw new Error('Cannot reschedule a task that has not reached its maximum retry attempts.') + } + await WIKI.db.knex('jobs') + .insert({ + id: job.id, + task: job.task, + useWorker: job.useWorker, + payload: job.payload, + retries: job.attempt, + maxRetries: job.maxRetries, + isScheduled: job.wasScheduled, + createdBy: WIKI.INSTANCE_ID + }) + WIKI.logger.info(`Job ${args.id} has been rescheduled [ OK ]`) + return { + operation: graphHelper.generateSuccess('Job rescheduled successfully.') + } + } catch (err) { + WIKI.logger.warn(err) + return graphHelper.generateError(err) + } + }, async updateSystemFlags (obj, args, context) { WIKI.config.flags = _.transform(args.flags, (result, row) => { _.set(result, row.key, row.value) diff --git a/server/graph/schemas/system.graphql b/server/graph/schemas/system.graphql index 8344d41c..c79428ae 100644 --- a/server/graph/schemas/system.graphql +++ b/server/graph/schemas/system.graphql @@ -16,12 +16,20 @@ extend type Query { } extend type Mutation { + cancelJob( + id: UUID! + ): DefaultResponse + disconnectWS: DefaultResponse installExtension( key: String! ): DefaultResponse + retryJob( + id: UUID! + ): DefaultResponse + updateSystemFlags( flags: [SystemFlagInput]! ): DefaultResponse diff --git a/server/helpers/common.js b/server/helpers/common.js index 44fd39f1..205e0dfc 100644 --- a/server/helpers/common.js +++ b/server/helpers/common.js @@ -1,6 +1,34 @@ const _ = require('lodash') module.exports = { + /* eslint-disable promise/param-names */ + createDeferred () { + let result, resolve, reject + return { + resolve: function (value) { + if (resolve) { + resolve(value) + } else { + result = result || new Promise(function (r) { r(value) }) + } + }, + reject: function (reason) { + if (reject) { + reject(reason) + } else { + result = result || new Promise(function (x, j) { j(reason) }) + } + }, + promise: new Promise(function (r, j) { + if (result) { + r(result) + } else { + resolve = r + reject = j + } + }) + } + }, /** * Get default value of type * diff --git a/server/models/editors.js b/server/models/editors.js deleted file mode 100644 index e24d00a2..00000000 --- a/server/models/editors.js +++ /dev/null @@ -1,41 +0,0 @@ -const Model = require('objection').Model - -/** - * Editor model - */ -module.exports = class Editor extends Model { - static get tableName() { return 'editors' } - 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 getEditors() { - return WIKI.db.editors.query() - } - - static async getDefaultEditor(contentType) { - // TODO - hardcoded for now - switch (contentType) { - case 'markdown': - return 'markdown' - case 'html': - return 'ckeditor' - default: - return 'code' - } - } -} diff --git a/server/models/pages.js b/server/models/pages.js index 60c7095d..583a1777 100644 --- a/server/models/pages.js +++ b/server/models/pages.js @@ -34,7 +34,7 @@ module.exports = class Page extends Model { required: ['path', 'title'], properties: { - id: {type: 'integer'}, + id: {type: 'string'}, path: {type: 'string'}, hash: {type: 'string'}, title: {type: 'string'}, @@ -44,7 +44,7 @@ module.exports = class Page extends Model { publishEndDate: {type: 'string'}, content: {type: 'string'}, contentType: {type: 'string'}, - + siteId: {type: 'string'}, createdAt: {type: 'string'}, updatedAt: {type: 'string'} } @@ -125,11 +125,11 @@ module.exports = class Page extends Model { */ static get cacheSchema() { return new JSBinType({ - id: 'uint', - authorId: 'uint', + id: 'string', + authorId: 'string', authorName: 'string', createdAt: 'string', - creatorId: 'uint', + creatorId: 'string', creatorName: 'string', description: 'string', editor: 'string', @@ -137,6 +137,7 @@ module.exports = class Page extends Model { publishEndDate: 'string', publishStartDate: 'string', render: 'string', + siteId: 'string', tags: [ { tag: 'string' @@ -291,7 +292,7 @@ module.exports = class Page extends Model { authorId: opts.user.id, content: opts.content, creatorId: opts.user.id, - contentType: _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text'), + contentType: _.get(WIKI.data.editors[opts.editor], 'contentType', 'text'), description: opts.description, editor: opts.editor, hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale }), @@ -322,6 +323,9 @@ module.exports = class Page extends Model { // -> Render page to HTML await WIKI.db.pages.renderPage(page) + return page + // TODO: Handle remaining flow + // -> Rebuild page tree await WIKI.db.pages.rebuildTree() @@ -922,12 +926,15 @@ module.exports = class Page extends Model { * @returns {Promise} Promise with no value */ static async renderPage(page) { - const renderJob = await WIKI.scheduler.registerJob({ - name: 'render-page', - immediate: true, - worker: true - }, page.id) - return renderJob.finished + const renderJob = await WIKI.scheduler.addJob({ + task: 'render-page', + payload: { + id: page.id + }, + maxRetries: 0, + promise: true + }) + return renderJob.promise } /** @@ -963,7 +970,7 @@ module.exports = class Page extends Model { * @returns {Promise} Promise of the Page Model Instance */ static async getPageFromDb(opts) { - const queryModeID = _.isNumber(opts) + const queryModeID = typeof opts === 'string' try { return WIKI.db.pages.query() .column([ @@ -985,6 +992,7 @@ module.exports = class Page extends Model { 'pages.localeCode', 'pages.authorId', 'pages.creatorId', + 'pages.siteId', 'pages.extra', { authorName: 'author.name', @@ -1033,7 +1041,7 @@ module.exports = class Page extends Model { id: page.id, authorId: page.authorId, authorName: page.authorName, - createdAt: page.createdAt, + createdAt: page.createdAt.toISOString(), creatorId: page.creatorId, creatorName: page.creatorName, description: page.description, @@ -1042,14 +1050,15 @@ module.exports = class Page extends Model { css: _.get(page, 'extra.css', ''), js: _.get(page, 'extra.js', '') }, - publishState: page.publishState, - publishEndDate: page.publishEndDate, - publishStartDate: page.publishStartDate, + publishState: page.publishState ?? '', + publishEndDate: page.publishEndDate ?? '', + publishStartDate: page.publishStartDate ?? '', render: page.render, + siteId: page.siteId, tags: page.tags.map(t => _.pick(t, ['tag'])), title: page.title, toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc), - updatedAt: page.updatedAt + updatedAt: page.updatedAt.toISOString() })) } diff --git a/server/models/renderers.js b/server/models/renderers.js index cad8226e..6e0cd037 100644 --- a/server/models/renderers.js +++ b/server/models/renderers.js @@ -55,19 +55,65 @@ module.exports = class Renderer extends Model { } static async refreshRenderersFromDisk() { - // const dbRenderers = await WIKI.db.renderers.query() + try { + const dbRenderers = await WIKI.db.renderers.query() + + // -> Fetch definitions from disk + await WIKI.db.renderers.fetchDefinitions() + + // -> Insert new Renderers + const newRenderers = [] + let updatedRenderers = 0 + for (const renderer of WIKI.data.renderers) { + if (!_.some(dbRenderers, ['module', renderer.key])) { + newRenderers.push({ + module: renderer.key, + isEnabled: renderer.enabledDefault ?? true, + config: _.transform(renderer.props, (result, value, key) => { + result[key] = value.default + return result + }, {}) + }) + } else { + const rendererConfig = _.get(_.find(dbRenderers, ['module', renderer.key]), 'config', {}) + await WIKI.db.renderers.query().patch({ + config: _.transform(renderer.props, (result, value, key) => { + if (!_.has(result, key)) { + result[key] = value.default + } + return result + }, rendererConfig) + }).where('module', renderer.key) + updatedRenderers++ + } + } + if (newRenderers.length > 0) { + await WIKI.db.renderers.query().insert(newRenderers) + WIKI.logger.info(`Loaded ${newRenderers.length} new renderers: [ OK ]`) + } - // -> Fetch definitions from disk - await WIKI.db.renderers.fetchDefinitions() + if (updatedRenderers > 0) { + WIKI.logger.info(`Updated ${updatedRenderers} existing renderers: [ OK ]`) + } - // TODO: Merge existing configs with updated modules + // -> Delete removed Renderers + for (const renderer of dbRenderers) { + if (!_.some(WIKI.data.renderers, ['key', renderer.module])) { + await WIKI.db.renderers.query().where('module', renderer.key).del() + WIKI.logger.info(`Removed renderer ${renderer.key} because it is no longer present in the modules folder: [ OK ]`) + } + } + } catch (err) { + WIKI.logger.error('Failed to import renderers: [ FAILED ]') + WIKI.logger.error(err) + } } static async getRenderingPipeline(contentType) { const renderersDb = await WIKI.db.renderers.query().where('isEnabled', true) if (renderersDb && renderersDb.length > 0) { const renderers = renderersDb.map(rdr => { - const renderer = _.find(WIKI.data.renderers, ['key', rdr.key]) + const renderer = _.find(WIKI.data.renderers, ['key', rdr.module]) return { ...renderer, config: rdr.config diff --git a/server/modules/rendering/html-asciinema/definition.yml b/server/modules/rendering/html-asciinema/definition.yml index 17358416..bda5496a 100644 --- a/server/modules/rendering/html-asciinema/definition.yml +++ b/server/modules/rendering/html-asciinema/definition.yml @@ -4,5 +4,5 @@ description: Embed asciinema players from compatible links author: requarks.io icon: mdi-theater enabledDefault: false -dependsOn: htmlCore +dependsOn: html-core props: {} diff --git a/server/modules/rendering/html-blockquotes/definition.yml b/server/modules/rendering/html-blockquotes/definition.yml index b24c8db6..5c43f825 100644 --- a/server/modules/rendering/html-blockquotes/definition.yml +++ b/server/modules/rendering/html-blockquotes/definition.yml @@ -4,5 +4,5 @@ description: Parse blockquotes box styling author: requarks.io icon: mdi-alpha-t-box-outline enabledDefault: true -dependsOn: htmlCore +dependsOn: html-core props: {} diff --git a/server/modules/rendering/html-codehighlighter/definition.yml b/server/modules/rendering/html-codehighlighter/definition.yml index 656931ff..075a56b8 100644 --- a/server/modules/rendering/html-codehighlighter/definition.yml +++ b/server/modules/rendering/html-codehighlighter/definition.yml @@ -4,6 +4,6 @@ description: Syntax detector for programming code author: requarks.io icon: mdi-code-braces enabledDefault: true -dependsOn: htmlCore +dependsOn: html-core step: pre props: {} diff --git a/server/modules/rendering/html-core/definition.yml b/server/modules/rendering/html-core/definition.yml index 2fe4783c..3ef3dd1f 100644 --- a/server/modules/rendering/html-core/definition.yml +++ b/server/modules/rendering/html-core/definition.yml @@ -1,4 +1,4 @@ -key: htmlCore +key: html-core title: Core description: Basic HTML Parser author: requarks.io diff --git a/server/modules/rendering/html-core/renderer.js b/server/modules/rendering/html-core/renderer.js index 972ef83f..73d43bed 100644 --- a/server/modules/rendering/html-core/renderer.js +++ b/server/modules/rendering/html-core/renderer.js @@ -21,7 +21,7 @@ module.exports = { // -------------------------------- for (let child of _.reject(this.children, ['step', 'post'])) { - const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`) + const renderer = require(`../${child.key}/renderer.js`) await renderer.init($, child.config) } @@ -33,10 +33,7 @@ module.exports = { const reservedPrefixes = /^\/[a-z]\//i const exactReservedPaths = /^\/[a-z]$/i - const isHostSet = WIKI.config.host.length > 7 && WIKI.config.host !== 'http://' - if (!isHostSet) { - WIKI.logger.warn('Host is not set. You must set the Site Host under General in the Administration Area!') - } + const hasHostname = this.site.hostname !== '*' $('a').each((i, elm) => { let href = $(elm).attr('href') @@ -48,8 +45,8 @@ module.exports = { } // -> Strip host from local links - if (isHostSet && href.indexOf(`${WIKI.config.host}/`) === 0) { - href = href.replace(WIKI.config.host, '') + if (hasHostname && href.indexOf(`${this.site.hostname}/`) === 0) { + href = href.replace(this.site.hostname, '') } // -> Assign local / external tag @@ -68,7 +65,7 @@ module.exports = { let pagePath = null // -> Add locale prefix if using namespacing - if (WIKI.config.lang.namespacing) { + if (this.site.config.localeNamespacing) { // -> Reformat paths if (href.indexOf('/') !== 0) { if (this.config.absoluteLinks) { diff --git a/server/modules/rendering/html-diagram/definition.yml b/server/modules/rendering/html-diagram/definition.yml index f850c7f0..2c7eeaf9 100644 --- a/server/modules/rendering/html-diagram/definition.yml +++ b/server/modules/rendering/html-diagram/definition.yml @@ -4,5 +4,5 @@ description: HTML Processing for diagrams (draw.io) author: requarks.io icon: mdi-chart-multiline enabledDefault: true -dependsOn: htmlCore +dependsOn: html-core props: {} diff --git a/server/modules/rendering/html-image-prefetch/definition.yml b/server/modules/rendering/html-image-prefetch/definition.yml index bf7a65df..09ebb028 100644 --- a/server/modules/rendering/html-image-prefetch/definition.yml +++ b/server/modules/rendering/html-image-prefetch/definition.yml @@ -4,5 +4,5 @@ description: Prefetch remotely rendered images (korki/plantuml) author: requarks.io icon: mdi-cloud-download-outline enabledDefault: false -dependsOn: htmlCore +dependsOn: html-core props: {} diff --git a/server/modules/rendering/html-mediaplayers/definition.yml b/server/modules/rendering/html-mediaplayers/definition.yml index 4b0201d0..e37ae900 100644 --- a/server/modules/rendering/html-mediaplayers/definition.yml +++ b/server/modules/rendering/html-mediaplayers/definition.yml @@ -4,5 +4,5 @@ description: Embed players such as Youtube, Vimeo, Soundcloud, etc. author: requarks.io icon: mdi-video enabledDefault: true -dependsOn: htmlCore +dependsOn: html-core props: {} diff --git a/server/modules/rendering/html-mermaid/definition.yml b/server/modules/rendering/html-mermaid/definition.yml index 2119a6b6..a0608dfa 100644 --- a/server/modules/rendering/html-mermaid/definition.yml +++ b/server/modules/rendering/html-mermaid/definition.yml @@ -4,5 +4,5 @@ description: Generate flowcharts from Mermaid syntax author: requarks.io icon: mdi-arrow-decision-outline enabledDefault: true -dependsOn: htmlCore +dependsOn: html-core props: {} diff --git a/server/modules/rendering/html-security/definition.yml b/server/modules/rendering/html-security/definition.yml index 04067c58..bcfcd7bb 100644 --- a/server/modules/rendering/html-security/definition.yml +++ b/server/modules/rendering/html-security/definition.yml @@ -4,7 +4,7 @@ description: Filter and strips potentially dangerous content author: requarks.io icon: mdi-fire enabledDefault: true -dependsOn: htmlCore +dependsOn: html-core step: post order: 99999 props: diff --git a/server/modules/rendering/html-tabset/definition.yml b/server/modules/rendering/html-tabset/definition.yml index 147b0128..281a8074 100644 --- a/server/modules/rendering/html-tabset/definition.yml +++ b/server/modules/rendering/html-tabset/definition.yml @@ -4,5 +4,5 @@ description: Transform headers into tabs author: requarks.io icon: mdi-tab enabledDefault: true -dependsOn: htmlCore +dependsOn: html-core props: {} diff --git a/server/modules/rendering/html-twemoji/definition.yml b/server/modules/rendering/html-twemoji/definition.yml index aedb9c0a..fe96ddc3 100644 --- a/server/modules/rendering/html-twemoji/definition.yml +++ b/server/modules/rendering/html-twemoji/definition.yml @@ -4,7 +4,7 @@ description: Apply Twitter Emojis to all Unicode emojis author: requarks.io icon: mdi-emoticon-happy-outline enabledDefault: true -dependsOn: htmlCore +dependsOn: html-core step: post order: 10 props: {} diff --git a/server/modules/rendering/markdown-abbr/definition.yml b/server/modules/rendering/markdown-abbr/definition.yml index d51822e2..f64ab46c 100644 --- a/server/modules/rendering/markdown-abbr/definition.yml +++ b/server/modules/rendering/markdown-abbr/definition.yml @@ -4,5 +4,5 @@ description: Parse abbreviations into abbr tags author: requarks.io icon: mdi-contain-start enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: {} diff --git a/server/modules/rendering/markdown-core/definition.yml b/server/modules/rendering/markdown-core/definition.yml index 7d3eea7b..17a35702 100644 --- a/server/modules/rendering/markdown-core/definition.yml +++ b/server/modules/rendering/markdown-core/definition.yml @@ -1,4 +1,4 @@ -key: markdownCore +key: markdown-core title: Core description: Basic Markdown Parser author: requarks.io diff --git a/server/modules/rendering/markdown-core/renderer.js b/server/modules/rendering/markdown-core/renderer.js index 42bd12be..8e686c6c 100644 --- a/server/modules/rendering/markdown-core/renderer.js +++ b/server/modules/rendering/markdown-core/renderer.js @@ -44,7 +44,7 @@ module.exports = { }) for (let child of this.children) { - const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`) + const renderer = require(`../${child.key}/renderer.js`) await renderer.init(mkdown, child.config) } diff --git a/server/modules/rendering/markdown-emoji/definition.yml b/server/modules/rendering/markdown-emoji/definition.yml index c1b8f36c..716cddee 100644 --- a/server/modules/rendering/markdown-emoji/definition.yml +++ b/server/modules/rendering/markdown-emoji/definition.yml @@ -4,5 +4,5 @@ description: Convert tags to emojis author: requarks.io icon: mdi-sticker-emoji enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: {} diff --git a/server/modules/rendering/markdown-expandtabs/definition.yml b/server/modules/rendering/markdown-expandtabs/definition.yml index 97b6990e..6531d2a8 100644 --- a/server/modules/rendering/markdown-expandtabs/definition.yml +++ b/server/modules/rendering/markdown-expandtabs/definition.yml @@ -4,7 +4,7 @@ description: Replace tabs with spaces in code blocks author: requarks.io icon: mdi-arrow-expand-horizontal enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: tabWidth: type: Number diff --git a/server/modules/rendering/markdown-footnotes/definition.yml b/server/modules/rendering/markdown-footnotes/definition.yml index d34d798b..00876294 100644 --- a/server/modules/rendering/markdown-footnotes/definition.yml +++ b/server/modules/rendering/markdown-footnotes/definition.yml @@ -4,5 +4,5 @@ description: Parse footnotes references author: requarks.io icon: mdi-page-layout-footer enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: {} diff --git a/server/modules/rendering/markdown-imsize/definition.yml b/server/modules/rendering/markdown-imsize/definition.yml index 6bafb563..603a3069 100644 --- a/server/modules/rendering/markdown-imsize/definition.yml +++ b/server/modules/rendering/markdown-imsize/definition.yml @@ -4,5 +4,5 @@ description: Adds dimensions attributes to images author: requarks.io icon: mdi-image-size-select-large enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: {} diff --git a/server/modules/rendering/markdown-katex/definition.yml b/server/modules/rendering/markdown-katex/definition.yml index 87b3b218..0532ea75 100644 --- a/server/modules/rendering/markdown-katex/definition.yml +++ b/server/modules/rendering/markdown-katex/definition.yml @@ -4,7 +4,7 @@ description: LaTeX Math + Chemical Expression Typesetting Renderer author: requarks.io icon: mdi-math-integral enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: useInline: type: Boolean diff --git a/server/modules/rendering/markdown-kroki/definition.yml b/server/modules/rendering/markdown-kroki/definition.yml index 09a77f49..b56d3785 100644 --- a/server/modules/rendering/markdown-kroki/definition.yml +++ b/server/modules/rendering/markdown-kroki/definition.yml @@ -4,7 +4,7 @@ description: Kroki Diagrams Parser author: rlanyi (based on PlantUML renderer) icon: mdi-sitemap enabledDefault: false -dependsOn: markdownCore +dependsOn: markdown-core props: server: type: String diff --git a/server/modules/rendering/markdown-mathjax/definition.yml b/server/modules/rendering/markdown-mathjax/definition.yml index bf2e6460..c0e95120 100644 --- a/server/modules/rendering/markdown-mathjax/definition.yml +++ b/server/modules/rendering/markdown-mathjax/definition.yml @@ -4,7 +4,7 @@ description: LaTeX Math + Chemical Expression Typesetting Renderer author: requarks.io icon: mdi-math-integral enabledDefault: false -dependsOn: markdownCore +dependsOn: markdown-core props: useInline: type: Boolean diff --git a/server/modules/rendering/markdown-multi-table/definition.yml b/server/modules/rendering/markdown-multi-table/definition.yml index d3cbe67a..57885052 100644 --- a/server/modules/rendering/markdown-multi-table/definition.yml +++ b/server/modules/rendering/markdown-multi-table/definition.yml @@ -4,7 +4,7 @@ description: Add MultiMarkdown table support author: requarks.io icon: mdi-table enabledDefault: false -dependsOn: markdownCore +dependsOn: markdown-core props: multilineEnabled: type: Boolean diff --git a/server/modules/rendering/markdown-plantuml/definition.yml b/server/modules/rendering/markdown-plantuml/definition.yml index e8b156fc..eb81d834 100644 --- a/server/modules/rendering/markdown-plantuml/definition.yml +++ b/server/modules/rendering/markdown-plantuml/definition.yml @@ -4,7 +4,7 @@ description: PlantUML Markdown Parser author: ethanmdavidson icon: mdi-sitemap enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: server: type: String diff --git a/server/modules/rendering/markdown-supsub/definition.yml b/server/modules/rendering/markdown-supsub/definition.yml index f037d417..ffb63b21 100644 --- a/server/modules/rendering/markdown-supsub/definition.yml +++ b/server/modules/rendering/markdown-supsub/definition.yml @@ -4,7 +4,7 @@ description: Parse subscript and superscript tags author: requarks.io icon: mdi-format-superscript enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: subEnabled: type: Boolean diff --git a/server/modules/rendering/markdown-tasklists/definition.yml b/server/modules/rendering/markdown-tasklists/definition.yml index aa2dae74..866ef99b 100644 --- a/server/modules/rendering/markdown-tasklists/definition.yml +++ b/server/modules/rendering/markdown-tasklists/definition.yml @@ -4,5 +4,5 @@ description: Parse task lists to checkboxes author: requarks.io icon: mdi-format-list-checks enabledDefault: true -dependsOn: markdownCore +dependsOn: markdown-core props: {} diff --git a/server/tasks/workers/render-page.js b/server/tasks/workers/render-page.js new file mode 100644 index 00000000..6c23cbf5 --- /dev/null +++ b/server/tasks/workers/render-page.js @@ -0,0 +1,92 @@ +const _ = require('lodash') +const cheerio = require('cheerio') + +module.exports = async ({ payload }) => { + WIKI.logger.info(`Rendering page ${payload.id}...`) + + try { + await WIKI.ensureDb() + + const page = await WIKI.db.pages.getPageFromDb(payload.id) + if (!page) { + throw new Error('Invalid Page Id') + } + + const site = await WIKI.db.sites.query().findById(page.siteId) + + await WIKI.db.renderers.fetchDefinitions() + + const pipeline = await WIKI.db.renderers.getRenderingPipeline(page.contentType) + + let output = page.content + + if (_.isEmpty(page.content)) { + WIKI.logger.warn(`Failed to render page ID ${payload.id} because content was empty: [ FAILED ]`) + } + + for (let core of pipeline) { + const renderer = require(`../../modules/rendering/${core.key}/renderer.js`) + output = await renderer.render.call({ + config: core.config, + children: core.children, + page, + site, + input: output + }) + } + + // Parse TOC + const $ = cheerio.load(output) + let isStrict = $('h1').length > 0 // <- Allows for documents using H2 as top level + let toc = { root: [] } + + $('h1,h2,h3,h4,h5,h6').each((idx, el) => { + const depth = _.toSafeInteger(el.name.substring(1)) - (isStrict ? 1 : 2) + let leafPathError = false + + const leafPath = _.reduce(_.times(depth), (curPath, curIdx) => { + if (_.has(toc, curPath)) { + const lastLeafIdx = _.get(toc, curPath).length - 1 + if (lastLeafIdx >= 0) { + curPath = `${curPath}[${lastLeafIdx}].children` + } else { + leafPathError = true + } + } + return curPath + }, 'root') + + if (leafPathError) { return } + + const leafSlug = $('.toc-anchor', el).first().attr('href') + $('.toc-anchor', el).remove() + + _.get(toc, leafPath).push({ + title: _.trim($(el).text()), + anchor: leafSlug, + children: [] + }) + }) + + // Save to DB + await WIKI.db.pages.query() + .patch({ + render: output, + toc: JSON.stringify(toc.root) + }) + .where('id', payload.id) + + // Save to cache + // await WIKI.db.pages.savePageToCache({ + // ...page, + // render: output, + // toc: JSON.stringify(toc.root) + // }) + + WIKI.logger.info(`Rendered page ${payload.id}: [ COMPLETED ]`) + } catch (err) { + WIKI.logger.error(`Rendering page ${payload.id}: [ FAILED ]`) + WIKI.logger.error(err.message) + throw err + } +} diff --git a/server/views/page.pug b/server/views/page.pug index 4cfd3eba..6fc2b4cc 100644 --- a/server/views/page.pug +++ b/server/views/page.pug @@ -5,8 +5,6 @@ block head style(type='text/css')!= injectCode.css if injectCode.head != injectCode.head - if config.features.featurePageComments - != comments.head block body #root @@ -21,20 +19,9 @@ block body author-name=page.authorName :author-id=page.authorId editor=page.editorKey - :is-published=page.isPublished.toString() - toc=Buffer.from(page.toc).toString('base64') :page-id=page.id - sidebar=Buffer.from(JSON.stringify(sidebar)).toString('base64') - nav-mode=config.nav.mode - comments-enabled=config.features.featurePageComments - effective-permissions=Buffer.from(JSON.stringify(effectivePermissions)).toString('base64') - comments-external=comments.codeTemplate ) template(slot='contents') div!= page.render - template(slot='comments') - div!= comments.main if injectCode.body != injectCode.body - if config.features.featurePageComments - != comments.body diff --git a/server/worker.js b/server/worker.js index 1b8b4e28..71456a73 100644 --- a/server/worker.js +++ b/server/worker.js @@ -12,7 +12,23 @@ let WIKI = { INSTANCE_ID: 'worker', SERVERPATH: path.join(process.cwd(), 'server'), Error: require('./helpers/error'), - configSvc: require('./core/config') + configSvc: require('./core/config'), + ensureDb: async () => { + if (WIKI.db) { return true } + + WIKI.db = require('./core/db').init(true) + + try { + await WIKI.configSvc.loadFromDb() + await WIKI.configSvc.applyFlags() + } catch (err) { + WIKI.logger.error('Database Initialization Error: ' + err.message) + if (WIKI.IS_DEBUG) { + WIKI.logger.error(err) + } + process.exit(1) + } + } } global.WIKI = WIKI diff --git a/ux/src/components/PageTags.vue b/ux/src/components/PageTags.vue index 3bcc77ca..733d1adf 100644 --- a/ux/src/components/PageTags.vue +++ b/ux/src/components/PageTags.vue @@ -1,21 +1,21 @@ - diff --git a/ux/src/components/SocialSharingMenu.vue b/ux/src/components/SocialSharingMenu.vue index af9603a4..858f9843 100644 --- a/ux/src/components/SocialSharingMenu.vue +++ b/ux/src/components/SocialSharingMenu.vue @@ -11,125 +11,146 @@ q-menu( q-item-section.items-center(avatar) q-icon(color='grey', name='las la-clipboard', size='sm') q-item-section.q-pr-md Copy URL - q-item(clickable, tag='a', :href='`mailto:?subject=` + encodeURIComponent(title) + `&body=` + encodeURIComponent(urlFormatted) + `%0D%0A%0D%0A` + encodeURIComponent(description)', target='_blank') + q-item(clickable, tag='a', :href='`mailto:?subject=` + encodeURIComponent(props.title) + `&body=` + encodeURIComponent(urlFormatted) + `%0D%0A%0D%0A` + encodeURIComponent(props.description)', target='_blank') q-item-section.items-center(avatar) q-icon(color='grey', name='las la-envelope', size='sm') q-item-section.q-pr-md Email - q-item(clickable, @click='openSocialPop(`https://www.facebook.com/sharer/sharer.php?u=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(title) + `&description=` + encodeURIComponent(description))') + q-item(clickable, @click='openSocialPop(`https://www.facebook.com/sharer/sharer.php?u=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(props.title) + `&description=` + encodeURIComponent(props.description))') q-item-section.items-center(avatar) q-icon(color='grey', name='lab la-facebook', size='sm') q-item-section.q-pr-md Facebook - q-item(clickable, @click='openSocialPop(`https://www.linkedin.com/shareArticle?mini=true&url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(title) + `&summary=` + encodeURIComponent(description))') + q-item(clickable, @click='openSocialPop(`https://www.linkedin.com/shareArticle?mini=true&url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(props.title) + `&summary=` + encodeURIComponent(props.description))') q-item-section.items-center(avatar) q-icon(color='grey', name='lab la-linkedin', size='sm') q-item-section.q-pr-md LinkedIn - q-item(clickable, @click='openSocialPop(`https://www.reddit.com/submit?url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(title))') + q-item(clickable, @click='openSocialPop(`https://www.reddit.com/submit?url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(props.title))') q-item-section.items-center(avatar) q-icon(color='grey', name='lab la-reddit', size='sm') q-item-section.q-pr-md Reddit - q-item(clickable, @click='openSocialPop(`https://t.me/share/url?url=` + encodeURIComponent(urlFormatted) + `&text=` + encodeURIComponent(title))') + q-item(clickable, @click='openSocialPop(`https://t.me/share/url?url=` + encodeURIComponent(urlFormatted) + `&text=` + encodeURIComponent(props.title))') q-item-section.items-center(avatar) q-icon(color='grey', name='lab la-telegram', size='sm') q-item-section.q-pr-md Telegram - q-item(clickable, @click='openSocialPop(`https://twitter.com/intent/tweet?url=` + encodeURIComponent(urlFormatted) + `&text=` + encodeURIComponent(title))') + q-item(clickable, @click='openSocialPop(`https://twitter.com/intent/tweet?url=` + encodeURIComponent(urlFormatted) + `&text=` + encodeURIComponent(props.title))') q-item-section.items-center(avatar) q-icon(color='grey', name='lab la-twitter', size='sm') q-item-section.q-pr-md Twitter - q-item(clickable, :href='`viber://forward?text=` + encodeURIComponent(urlFormatted) + ` ` + encodeURIComponent(description)') + q-item(clickable, :href='`viber://forward?text=` + encodeURIComponent(urlFormatted) + ` ` + encodeURIComponent(props.description)') q-item-section.items-center(avatar) q-icon(color='grey', name='lab la-viber', size='sm') q-item-section.q-pr-md Viber - q-item(clickable, @click='openSocialPop(`http://service.weibo.com/share/share.php?url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(title))') + q-item(clickable, @click='openSocialPop(`http://service.weibo.com/share/share.php?url=` + encodeURIComponent(urlFormatted) + `&title=` + encodeURIComponent(props.title))') q-item-section.items-center(avatar) q-icon(color='grey', name='lab la-weibo', size='sm') q-item-section.q-pr-md Weibo - q-item(clickable, @click='openSocialPop(`https://api.whatsapp.com/send?text=` + encodeURIComponent(title) + `%0D%0A` + encodeURIComponent(urlFormatted))') + q-item(clickable, @click='openSocialPop(`https://api.whatsapp.com/send?text=` + encodeURIComponent(props.title) + `%0D%0A` + encodeURIComponent(urlFormatted))') q-item-section.items-center(avatar) q-icon(color='grey', name='lab la-whatsapp', size='sm') q-item-section.q-pr-md Whatsapp - diff --git a/ux/src/i18n/locales/en.json b/ux/src/i18n/locales/en.json index fbbe1fa6..1114ccf8 100644 --- a/ux/src/i18n/locales/en.json +++ b/ux/src/i18n/locales/en.json @@ -1540,5 +1540,9 @@ "admin.instances.lastSeen": "Last Seen", "admin.instances.firstSeen": "First Seen", "admin.instances.activeListeners": "Active Listeners", - "admin.instances.activeConnections": "Active Connections" + "admin.instances.activeConnections": "Active Connections", + "admin.scheduler.cancelJob": "Cancel Job", + "admin.scheduler.cancelJobSuccess": "Job cancelled successfully.", + "admin.scheduler.retryJob": "Retry Job", + "admin.scheduler.retryJobSuccess": "Job has been rescheduled and will execute shortly." } diff --git a/ux/src/layouts/AdminLayout.vue b/ux/src/layouts/AdminLayout.vue index 2129e9ff..8974e191 100644 --- a/ux/src/layouts/AdminLayout.vue +++ b/ux/src/layouts/AdminLayout.vue @@ -3,7 +3,7 @@ q-layout.admin(view='hHh Lpr lff') q-header.bg-black.text-white .row.no-wrap q-toolbar(style='height: 64px;', dark) - q-btn(dense, flat, href='/') + q-btn(dense, flat, to='/') q-avatar(size='34px', square) img(src='/_assets/logo-wikijs.svg') q-toolbar-title.text-h6 Wiki.js @@ -102,10 +102,6 @@ q-layout.admin(view='hHh Lpr lff') q-item-section(avatar) q-icon(name='img:/_assets/icons/fluent-tree-structure.svg') q-item-section {{ t('admin.navigation.title') }} - q-item(:to='`/_admin/` + adminStore.currentSiteId + `/rendering`', v-ripple, active-class='bg-primary text-white', disabled) - q-item-section(avatar) - q-icon(name='img:/_assets/icons/fluent-rich-text-converter.svg') - q-item-section {{ t('admin.rendering.title') }} q-item(:to='`/_admin/` + adminStore.currentSiteId + `/storage`', v-ripple, active-class='bg-primary text-white') q-item-section(avatar) q-icon(name='img:/_assets/icons/fluent-ssd.svg') @@ -156,6 +152,10 @@ q-layout.admin(view='hHh Lpr lff') q-item-section {{ t('admin.mail.title') }} q-item-section(side) status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`') + q-item(to='/_admin/rendering', v-ripple, active-class='bg-primary text-white', disabled) + q-item-section(avatar) + q-icon(name='img:/_assets/icons/fluent-rich-text-converter.svg') + q-item-section {{ t('admin.rendering.title') }} q-item(to='/_admin/scheduler', v-ripple, active-class='bg-primary text-white') q-item-section(avatar) q-icon(name='img:/_assets/icons/fluent-bot.svg') diff --git a/ux/src/layouts/MainLayout.vue b/ux/src/layouts/MainLayout.vue index 25f7f5dc..d6824d96 100644 --- a/ux/src/layouts/MainLayout.vue +++ b/ux/src/layouts/MainLayout.vue @@ -2,7 +2,7 @@ q-layout(view='hHh Lpr lff') header-nav q-drawer.bg-sidebar( - v-model='showSideNav' + v-model='siteStore.showSideNav' show-if-above :width='255' ) @@ -81,46 +81,64 @@ q-layout(view='hHh Lpr lff') span(style='font-size: 11px;') © Cyberdyne Systems Corp. 2020 | Powered by #[strong Wiki.js] -