mirror of
https://github.com/requarks/wiki.git
synced 2026-08-26 13:17:10 -05:00
feat: rendering + new page view
This commit is contained in:
+8
-1
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+8
-4
@@ -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()
|
||||
|
||||
+72
-11
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-18
@@ -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()
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
// -> Fetch definitions from disk
|
||||
await WIKI.db.renderers.fetchDefinitions()
|
||||
|
||||
// TODO: Merge existing configs with updated modules
|
||||
// -> 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 ]`)
|
||||
}
|
||||
|
||||
if (updatedRenderers > 0) {
|
||||
WIKI.logger.info(`Updated ${updatedRenderers} existing renderers: [ OK ]`)
|
||||
}
|
||||
|
||||
// -> 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
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
key: htmlCore
|
||||
key: html-core
|
||||
title: Core
|
||||
description: Basic HTML Parser
|
||||
author: requarks.io
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -4,5 +4,5 @@ description: Transform headers into tabs
|
||||
author: requarks.io
|
||||
icon: mdi-tab
|
||||
enabledDefault: true
|
||||
dependsOn: htmlCore
|
||||
dependsOn: html-core
|
||||
props: {}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
key: markdownCore
|
||||
key: markdown-core
|
||||
title: Core
|
||||
description: Basic Markdown Parser
|
||||
author: requarks.io
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ description: Convert tags to emojis
|
||||
author: requarks.io
|
||||
icon: mdi-sticker-emoji
|
||||
enabledDefault: true
|
||||
dependsOn: markdownCore
|
||||
dependsOn: markdown-core
|
||||
props: {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,5 +4,5 @@ description: Parse footnotes references
|
||||
author: requarks.io
|
||||
icon: mdi-page-layout-footer
|
||||
enabledDefault: true
|
||||
dependsOn: markdownCore
|
||||
dependsOn: markdown-core
|
||||
props: {}
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ description: PlantUML Markdown Parser
|
||||
author: ethanmdavidson
|
||||
icon: mdi-sitemap
|
||||
enabledDefault: true
|
||||
dependsOn: markdownCore
|
||||
dependsOn: markdown-core
|
||||
props:
|
||||
server:
|
||||
type: String
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+17
-1
@@ -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
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template lang="pug">
|
||||
.q-gutter-xs
|
||||
template(v-if='tags && tags.length > 0')
|
||||
template(v-if='pageStore.tags && pageStore.tags.length > 0')
|
||||
q-chip(
|
||||
square
|
||||
color='secondary'
|
||||
text-color='white'
|
||||
dense
|
||||
clickable
|
||||
:removable='edit'
|
||||
:removable='props.edit'
|
||||
@remove='removeTag(tag)'
|
||||
v-for='tag of tags'
|
||||
v-for='tag of pageStore.tags'
|
||||
:key='`tag-` + tag'
|
||||
)
|
||||
q-icon.q-mr-xs(name='las la-tag', size='14px')
|
||||
span.text-caption {{tag}}
|
||||
q-chip(
|
||||
v-if='!edit && tags.length > 1'
|
||||
v-if='!props.edit && pageStore.tags.length > 1'
|
||||
square
|
||||
color='secondary'
|
||||
text-color='white'
|
||||
@@ -24,36 +24,51 @@
|
||||
)
|
||||
q-icon(name='las la-tags', size='14px')
|
||||
q-input.q-mt-md(
|
||||
v-if='edit'
|
||||
v-if='props.edit'
|
||||
outlined
|
||||
v-model='newTag'
|
||||
v-model='state.newTag'
|
||||
dense
|
||||
placeholder='Add new tag...'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { sync } from 'vuex-pathify'
|
||||
<script setup>
|
||||
import { useQuasar } from 'quasar'
|
||||
import { reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
newTag: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tags: sync('page/tags', false)
|
||||
},
|
||||
methods: {
|
||||
removeTag (tag) {
|
||||
this.tags = this.tags.filter(t => t !== tag)
|
||||
}
|
||||
import { usePageStore } from 'src/stores/page'
|
||||
|
||||
// PROPS
|
||||
|
||||
const props = defineProps({
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// QUASAR
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
// STORES
|
||||
|
||||
const pageStore = usePageStore()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
newTag: ''
|
||||
})
|
||||
|
||||
// METHODS
|
||||
|
||||
function removeTag (tag) {
|
||||
pageStore.tags = pageStore.tags.filter(t => t !== tag)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import ClipboardJS from 'clipboard'
|
||||
import { useQuasar } from 'quasar'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Untitled Page'
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
// PROPS
|
||||
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
width: 626,
|
||||
height: 436,
|
||||
left: 0,
|
||||
top: 0,
|
||||
clip: null
|
||||
}
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Untitled Page'
|
||||
},
|
||||
computed: {
|
||||
urlFormatted () {
|
||||
if (!import.meta.env.SSR) {
|
||||
return this.url ? this.url : window.location.href
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openSocialPop (url) {
|
||||
const popupWindow = window.open(
|
||||
url,
|
||||
'sharer',
|
||||
`status=no,height=${this.height},width=${this.width},resizable=yes,left=${this.left},top=${this.top},screenX=${this.left},screenY=${this.top},toolbar=no,menubar=no,scrollbars=no,location=no,directories=no`
|
||||
)
|
||||
|
||||
popupWindow.focus()
|
||||
},
|
||||
menuShown (ev) {
|
||||
this.clip = new ClipboardJS(this.$refs.copyUrlButton.$el, {
|
||||
text: () => { return this.urlFormatted }
|
||||
})
|
||||
|
||||
this.clip.on('success', () => {
|
||||
this.$q.notify({
|
||||
message: 'URL copied successfully',
|
||||
icon: 'las la-clipboard'
|
||||
})
|
||||
})
|
||||
this.clip.on('error', () => {
|
||||
this.$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Failed to copy to clipboard'
|
||||
})
|
||||
})
|
||||
},
|
||||
menuHidden (ev) {
|
||||
this.clip.destroy()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
/**
|
||||
* Center the popup on dual screens
|
||||
* http://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen/32261263
|
||||
*/
|
||||
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
|
||||
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top
|
||||
|
||||
const width = window.innerWidth ? window.innerWidth : (document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width)
|
||||
const height = window.innerHeight ? window.innerHeight : (document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height)
|
||||
|
||||
this.left = ((width / 2) - (this.width / 2)) + dualScreenLeft
|
||||
this.top = ((height / 2) - (this.height / 2)) + dualScreenTop
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
// QUASAR
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
width: 626,
|
||||
height: 436,
|
||||
left: 0,
|
||||
top: 0,
|
||||
clip: null
|
||||
})
|
||||
let clip = null
|
||||
|
||||
const copyUrlButton = ref(null)
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const urlFormatted = computed(() => {
|
||||
if (!import.meta.env.SSR) {
|
||||
return props.url ? props.url : window.location.href
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
// METHODS
|
||||
|
||||
function openSocialPop (url) {
|
||||
const popupWindow = window.open(
|
||||
url,
|
||||
'sharer',
|
||||
`status=no,height=${state.height},width=${state.width},resizable=yes,left=${state.left},top=${state.top},screenX=${state.left},screenY=${state.top},toolbar=no,menubar=no,scrollbars=no,location=no,directories=no`
|
||||
)
|
||||
|
||||
popupWindow.focus()
|
||||
}
|
||||
|
||||
function menuShown (ev) {
|
||||
clip = new ClipboardJS(copyUrlButton.value.$el, {
|
||||
text: () => { return urlFormatted.value }
|
||||
})
|
||||
|
||||
clip.on('success', () => {
|
||||
$q.notify({
|
||||
message: 'URL copied successfully',
|
||||
icon: 'las la-clipboard'
|
||||
})
|
||||
})
|
||||
clip.on('error', () => {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Failed to copy to clipboard'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function menuHidden (ev) {
|
||||
clip.destroy()
|
||||
}
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
/**
|
||||
* Center the popup on dual screens
|
||||
* http://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen/32261263
|
||||
*/
|
||||
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left
|
||||
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top
|
||||
|
||||
const width = window.innerWidth ? window.innerWidth : (document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width)
|
||||
const height = window.innerHeight ? window.innerHeight : (document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height)
|
||||
|
||||
state.left = ((width / 2) - (state.width / 2)) + dualScreenLeft
|
||||
state.top = ((height / 2) - (state.height / 2)) + dualScreenTop
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get, sync } from 'vuex-pathify'
|
||||
import { setCssVar } from 'quasar'
|
||||
<script setup>
|
||||
import { useMeta, useQuasar, setCssVar } from 'quasar'
|
||||
import { defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSiteStore } from '../stores/site'
|
||||
|
||||
// COMPONENTS
|
||||
|
||||
import AccountMenu from '../components/AccountMenu.vue'
|
||||
import HeaderNav from '../components/HeaderNav.vue'
|
||||
|
||||
export default {
|
||||
name: 'MainLayout',
|
||||
components: {
|
||||
HeaderNav
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
leftDrawerOpen: true,
|
||||
search: '',
|
||||
thumbStyle: {
|
||||
right: '2px',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#FFF',
|
||||
width: '5px',
|
||||
opacity: 0.5
|
||||
},
|
||||
barStyle: {
|
||||
backgroundColor: '#000',
|
||||
width: '9px',
|
||||
opacity: 0.1
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showSideNav: sync('site/showSideNav', false),
|
||||
isSyncing: get('isLoading', false)
|
||||
},
|
||||
created () {
|
||||
setCssVar('primary', this.$store.get('site/theme@colorPrimary'))
|
||||
setCssVar('secondary', this.$store.get('site/theme@colorSecondary'))
|
||||
setCssVar('accent', this.$store.get('site/theme@colorAccent'))
|
||||
setCssVar('header', this.$store.get('site/theme@colorHeader'))
|
||||
setCssVar('sidebar', this.$store.get('site/theme@colorSidebar'))
|
||||
}
|
||||
// QUASAR
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
// STORES
|
||||
|
||||
const siteStore = useSiteStore()
|
||||
|
||||
// ROUTER
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// META
|
||||
|
||||
useMeta({
|
||||
titleTemplate: title => `${title} - ${siteStore.title}`
|
||||
})
|
||||
|
||||
// DATA
|
||||
|
||||
const leftDrawerOpen = ref(true)
|
||||
const search = ref('')
|
||||
const user = reactive({
|
||||
name: 'John Doe',
|
||||
email: 'test@example.com',
|
||||
picture: null
|
||||
})
|
||||
const thumbStyle = {
|
||||
right: '2px',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#FFF',
|
||||
width: '5px',
|
||||
opacity: 0.5
|
||||
}
|
||||
const barStyle = {
|
||||
backgroundColor: '#000',
|
||||
width: '9px',
|
||||
opacity: 0.1
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -132,7 +132,7 @@ q-page.admin-terminal
|
||||
div: small.text-grey {{humanizeDate(props.row.waitUntil)}}
|
||||
template(v-slot:body-cell-retries='props')
|
||||
q-td(:props='props')
|
||||
span #[strong {{props.value + 1}}] #[span.text-grey / {{props.row.maxRetries}}]
|
||||
span #[strong {{props.value + 1}}] #[span.text-grey / {{props.row.maxRetries + 1}}]
|
||||
template(v-slot:body-cell-useworker='props')
|
||||
q-td(:props='props')
|
||||
template(v-if='props.value')
|
||||
@@ -148,6 +148,15 @@ q-page.admin-terminal
|
||||
i18n-t.text-grey(keypath='admin.scheduler.createdBy', tag='small')
|
||||
template(#instance)
|
||||
strong {{props.row.createdBy}}
|
||||
template(v-slot:body-cell-cancel='props')
|
||||
q-td(:props='props')
|
||||
q-btn.acrylic-btn.q-px-sm(
|
||||
flat
|
||||
icon='las la-window-close'
|
||||
color='negative'
|
||||
@click='cancelJob(props.row.id)'
|
||||
)
|
||||
q-tooltip(anchor='center left', self='center right') {{ t('admin.scheduler.cancelJob') }}
|
||||
template(v-else)
|
||||
q-card.rounded-borders(
|
||||
v-if='state.jobs.length < 1'
|
||||
@@ -221,7 +230,7 @@ q-page.admin-terminal
|
||||
div: small {{ props.row.lastErrorMessage }}
|
||||
template(v-slot:body-cell-attempt='props')
|
||||
q-td(:props='props')
|
||||
span #[strong {{props.value}}] #[span.text-grey / {{props.row.maxRetries}}]
|
||||
span #[strong {{props.value}}] #[span.text-grey / {{props.row.maxRetries + 1}}]
|
||||
template(v-slot:body-cell-useworker='props')
|
||||
q-td(:props='props')
|
||||
template(v-if='props.value')
|
||||
@@ -238,6 +247,17 @@ q-page.admin-terminal
|
||||
i18n-t.text-grey(keypath='admin.scheduler.createdBy', tag='small')
|
||||
template(#instance)
|
||||
strong {{props.row.executedBy}}
|
||||
template(v-slot:body-cell-actions='props')
|
||||
q-td(:props='props')
|
||||
q-btn.acrylic-btn.q-px-sm(
|
||||
v-if='props.row.state !== `active`'
|
||||
flat
|
||||
icon='las la-undo-alt'
|
||||
color='orange'
|
||||
@click='retryJob(props.row.id)'
|
||||
:disable='props.row.state === `interrupted` || props.row.state === `failed` && props.row.attempt < props.row.maxRetries'
|
||||
)
|
||||
q-tooltip(anchor='center left', self='center right') {{ t('admin.scheduler.retryJob') }}
|
||||
|
||||
</template>
|
||||
|
||||
@@ -271,7 +291,7 @@ useMeta({
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
displayMode: 'completed',
|
||||
displayMode: 'upcoming',
|
||||
scheduledJobs: [],
|
||||
upcomingJobs: [],
|
||||
jobs: [],
|
||||
@@ -369,6 +389,13 @@ const upcomingJobsHeaders = [
|
||||
name: 'date',
|
||||
sortable: true,
|
||||
format: v => DateTime.fromISO(v).toRelative()
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
field: 'id',
|
||||
name: 'cancel',
|
||||
sortable: false,
|
||||
style: 'width: 15px;'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -415,6 +442,13 @@ const jobsHeaders = [
|
||||
name: 'date',
|
||||
sortable: true,
|
||||
format: v => DateTime.fromISO(v).toRelative()
|
||||
},
|
||||
{
|
||||
align: 'center',
|
||||
field: 'id',
|
||||
name: 'actions',
|
||||
sortable: false,
|
||||
style: 'width: 15px;'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -524,6 +558,80 @@ async function load () {
|
||||
state.loading--
|
||||
}
|
||||
|
||||
async function cancelJob (jobId) {
|
||||
state.loading++
|
||||
try {
|
||||
const resp = await APOLLO_CLIENT.mutate({
|
||||
mutation: gql`
|
||||
mutation cancelJob ($id: UUID!) {
|
||||
cancelJob(id: $id) {
|
||||
operation {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: jobId
|
||||
}
|
||||
})
|
||||
if (resp?.data?.cancelJob?.operation?.succeeded) {
|
||||
this.load()
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: t('admin.scheduler.cancelJobSuccess')
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp?.data?.cancelJob?.operation?.message || 'An unexpected error occured.')
|
||||
}
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Failed to cancel job.',
|
||||
caption: err.message
|
||||
})
|
||||
}
|
||||
state.loading--
|
||||
}
|
||||
|
||||
async function retryJob (jobId) {
|
||||
state.loading++
|
||||
try {
|
||||
const resp = await APOLLO_CLIENT.mutate({
|
||||
mutation: gql`
|
||||
mutation retryJob ($id: UUID!) {
|
||||
retryJob(id: $id) {
|
||||
operation {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: jobId
|
||||
}
|
||||
})
|
||||
if (resp?.data?.retryJob?.operation?.succeeded) {
|
||||
this.load()
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: t('admin.scheduler.retryJobSuccess')
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp?.data?.retryJob?.operation?.message || 'An unexpected error occured.')
|
||||
}
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Failed to retry the job.',
|
||||
caption: err.message
|
||||
})
|
||||
}
|
||||
state.loading--
|
||||
}
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
+206
-255
@@ -13,97 +13,33 @@ q-page.column
|
||||
q-breadcrumbs-el(icon='las la-home', to='/', aria-label='Home')
|
||||
q-tooltip Home
|
||||
q-breadcrumbs-el(
|
||||
v-for='brd of breadcrumbs'
|
||||
v-for='brd of pageStore.breadcrumbs'
|
||||
:key='brd.id'
|
||||
:icon='brd.icon'
|
||||
:label='brd.title'
|
||||
:aria-label='brd.title'
|
||||
:to='$pageHelpers.getFullPath(brd)'
|
||||
)
|
||||
q-breadcrumbs-el(
|
||||
v-if='editCreateMode'
|
||||
:icon='pageIcon'
|
||||
:label='title || `Untitled Page`'
|
||||
:aria-label='title || `Untitled Page`'
|
||||
:to='getFullPath(brd)'
|
||||
)
|
||||
.col-auto.flex.items-center.justify-end
|
||||
template(v-if='!isPublished')
|
||||
template(v-if='!pageStore.isPublished')
|
||||
.text-caption.text-accent: strong Unpublished
|
||||
q-separator.q-mx-sm(vertical)
|
||||
.text-caption.text-grey-6(v-if='editCreateMode') New Page
|
||||
.text-caption.text-grey-6(v-if='!editCreateMode') Last modified on #[strong September 5th, 2020]
|
||||
.text-caption.text-grey-6 Last modified on #[strong September 5th, 2020]
|
||||
.page-header.row
|
||||
//- PAGE ICON
|
||||
.col-auto.q-pl-md.flex.items-center(v-if='editMode')
|
||||
q-btn.rounded-borders(
|
||||
padding='none'
|
||||
size='37px'
|
||||
:icon='pageIcon'
|
||||
color='primary'
|
||||
flat
|
||||
)
|
||||
q-menu(content-class='shadow-7')
|
||||
icon-picker-dialog(v-model='pageIcon')
|
||||
.col-auto.q-pl-md.flex.items-center(v-else)
|
||||
.col-auto.q-pl-md.flex.items-center
|
||||
q-icon.rounded-borders(
|
||||
:name='pageIcon'
|
||||
:name='pageStore.icon'
|
||||
size='64px'
|
||||
color='primary'
|
||||
)
|
||||
//- PAGE HEADER
|
||||
.col.q-pa-md(v-if='editMode')
|
||||
q-input.no-height(
|
||||
borderless
|
||||
v-model='title'
|
||||
input-class='text-h4 text-grey-9'
|
||||
input-style='padding: 0;'
|
||||
placeholder='Untitled Page'
|
||||
hide-hint
|
||||
)
|
||||
q-input.no-height(
|
||||
borderless
|
||||
v-model='description'
|
||||
input-class='text-subtitle2 text-grey-7'
|
||||
input-style='padding: 0;'
|
||||
placeholder='Enter a short description'
|
||||
hide-hint
|
||||
)
|
||||
.col.q-pa-md(v-else)
|
||||
.text-h4.page-header-title {{title}}
|
||||
.text-subtitle2.page-header-subtitle {{description}}
|
||||
.col.q-pa-md
|
||||
.text-h4.page-header-title {{pageStore.title}}
|
||||
.text-subtitle2.page-header-subtitle {{pageStore.description}}
|
||||
|
||||
//- PAGE ACTIONS
|
||||
.col-auto.q-pa-md.flex.items-center.justify-end(v-if='editMode')
|
||||
q-btn.q-mr-sm.acrylic-btn(
|
||||
flat
|
||||
icon='las la-times'
|
||||
color='grey-7'
|
||||
label='Discard'
|
||||
aria-label='Discard'
|
||||
no-caps
|
||||
@click='mode = `view`'
|
||||
)
|
||||
q-btn(
|
||||
v-if='editorMode === `edit`'
|
||||
unelevated
|
||||
icon='las la-check'
|
||||
color='secondary'
|
||||
label='Save'
|
||||
aria-label='Save'
|
||||
no-caps
|
||||
@click='mode = `view`'
|
||||
)
|
||||
q-btn(
|
||||
v-else
|
||||
unelevated
|
||||
icon='las la-check'
|
||||
color='secondary'
|
||||
label='Create'
|
||||
aria-label='Create'
|
||||
no-caps
|
||||
@click='mode = `view`'
|
||||
)
|
||||
.col-auto.q-pa-md.flex.items-center.justify-end(v-else)
|
||||
.col-auto.q-pa-md.flex.items-center.justify-end
|
||||
q-btn.q-mr-md(
|
||||
flat
|
||||
dense
|
||||
@@ -144,23 +80,18 @@ q-page.column
|
||||
label='Edit'
|
||||
aria-label='Edit'
|
||||
no-caps
|
||||
@click='mode = `edit`'
|
||||
:href='editUrl'
|
||||
)
|
||||
.page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
|
||||
.col(style='order: 1;')
|
||||
q-no-ssr(v-if='editMode')
|
||||
component(:is='editorComponent')
|
||||
//- editor-wysiwyg
|
||||
//- editor-markdown
|
||||
q-scroll-area(
|
||||
:thumb-style='thumbStyle'
|
||||
:bar-style='barStyle'
|
||||
style='height: 100%;'
|
||||
v-else
|
||||
)
|
||||
.q-pa-md
|
||||
div(v-html='render')
|
||||
template(v-if='relations && relations.length > 0')
|
||||
div(v-html='pageStore.render')
|
||||
template(v-if='pageStore.relations && pageStore.relations.length > 0')
|
||||
q-separator.q-my-lg
|
||||
.row.align-center
|
||||
.col.text-left(v-if='relationsLeft.length > 0')
|
||||
@@ -204,24 +135,24 @@ q-page.column
|
||||
v-if='showSidebar'
|
||||
style='order: 2;'
|
||||
)
|
||||
template(v-if='showToc')
|
||||
template(v-if='pageStore.showToc')
|
||||
//- TOC
|
||||
.q-pa-md.flex.items-center
|
||||
q-icon.q-mr-sm(name='las la-stream', color='grey')
|
||||
.text-caption.text-grey-7 Contents
|
||||
.q-px-md.q-pb-sm
|
||||
q-tree(
|
||||
:nodes='toc'
|
||||
:nodes='state.toc'
|
||||
node-key='key'
|
||||
v-model:expanded='tocExpanded'
|
||||
v-model:selected='tocSelected'
|
||||
v-model:expanded='state.tocExpanded'
|
||||
v-model:selected='state.tocSelected'
|
||||
)
|
||||
//- Tags
|
||||
template(v-if='showTags')
|
||||
q-separator(v-if='showToc')
|
||||
template(v-if='pageStore.showTags')
|
||||
q-separator(v-if='pageStore.showToc')
|
||||
.q-pa-md(
|
||||
@mouseover='showTagsEditBtn = true'
|
||||
@mouseleave='showTagsEditBtn = false'
|
||||
@mouseover='state.showTagsEditBtn = true'
|
||||
@mouseleave='state.showTagsEditBtn = false'
|
||||
)
|
||||
.flex.items-center
|
||||
q-icon.q-mr-sm(name='las la-tags', color='grey')
|
||||
@@ -229,7 +160,7 @@ q-page.column
|
||||
q-space
|
||||
transition(name='fade')
|
||||
q-btn(
|
||||
v-show='showTagsEditBtn'
|
||||
v-show='state.showTagsEditBtn'
|
||||
size='sm'
|
||||
padding='none xs'
|
||||
icon='las la-pen'
|
||||
@@ -237,24 +168,24 @@ q-page.column
|
||||
flat
|
||||
label='Edit'
|
||||
no-caps
|
||||
@click='tagEditMode = !tagEditMode'
|
||||
@click='state.tagEditMode = !state.tagEditMode'
|
||||
)
|
||||
page-tags.q-mt-sm(:edit='tagEditMode')
|
||||
template(v-if='allowRatings && ratingsMode !== `off`')
|
||||
q-separator(v-if='showToc || showTags')
|
||||
page-tags.q-mt-sm(:edit='state.tagEditMode')
|
||||
template(v-if='pageStore.allowRatings && pageStore.ratingsMode !== `off`')
|
||||
q-separator(v-if='pageStore.showToc || pageStore.showTags')
|
||||
//- Rating
|
||||
.q-pa-md.flex.items-center
|
||||
q-icon.q-mr-sm(name='las la-star-half-alt', color='grey')
|
||||
.text-caption.text-grey-7 Rate this page
|
||||
.q-px-md
|
||||
q-rating(
|
||||
v-if='ratingsMode === `stars`'
|
||||
v-model='currentRating'
|
||||
v-if='pageStore.ratingsMode === `stars`'
|
||||
v-model='state.currentRating'
|
||||
icon='las la-star'
|
||||
color='secondary'
|
||||
size='sm'
|
||||
)
|
||||
.flex.items-center(v-else-if='ratingsMode === `thumbs`')
|
||||
.flex.items-center(v-else-if='pageStore.ratingsMode === `thumbs`')
|
||||
q-btn.acrylic-btn(
|
||||
flat
|
||||
icon='las la-thumbs-down'
|
||||
@@ -350,206 +281,226 @@ q-page.column
|
||||
q-tooltip(anchor='center left' self='center right') Delete Page
|
||||
|
||||
q-dialog(
|
||||
v-model='showSideDialog'
|
||||
v-model='state.showSideDialog'
|
||||
position='right'
|
||||
full-height
|
||||
transition-show='jump-left'
|
||||
transition-hide='jump-right'
|
||||
class='floating-sidepanel'
|
||||
)
|
||||
component(:is='sideDialogComponent')
|
||||
component(:is='state.sideDialogComponent')
|
||||
|
||||
q-dialog(
|
||||
v-model='showGlobalDialog'
|
||||
v-model='state.showGlobalDialog'
|
||||
transition-show='jump-up'
|
||||
transition-hide='jump-down'
|
||||
)
|
||||
component(:is='globalDialogComponent')
|
||||
component(:is='state.globalDialogComponent')
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { get, sync } from 'vuex-pathify'
|
||||
import IconPickerDialog from '../components/IconPickerDialog.vue'
|
||||
<script setup>
|
||||
import { useMeta, useQuasar, setCssVar } from 'quasar'
|
||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { usePageStore } from 'src/stores/page'
|
||||
import { useSiteStore } from '../stores/site'
|
||||
|
||||
// COMPONENTS
|
||||
|
||||
import SocialSharingMenu from '../components/SocialSharingMenu.vue'
|
||||
import PageDataDialog from '../components/PageDataDialog.vue'
|
||||
import PageTags from '../components/PageTags.vue'
|
||||
import PagePropertiesDialog from '../components/PagePropertiesDialog.vue'
|
||||
import PageSaveDialog from '../components/PageSaveDialog.vue'
|
||||
import EditorWysiwyg from '../components/EditorWysiwyg.vue'
|
||||
|
||||
export default {
|
||||
name: 'PageIndex',
|
||||
components: {
|
||||
EditorWysiwyg,
|
||||
IconPickerDialog,
|
||||
PageDataDialog,
|
||||
PagePropertiesDialog,
|
||||
PageSaveDialog,
|
||||
PageTags,
|
||||
SocialSharingMenu
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showSideDialog: false,
|
||||
sideDialogComponent: null,
|
||||
showGlobalDialog: false,
|
||||
globalDialogComponent: null,
|
||||
showTagsEditBtn: false,
|
||||
tagEditMode: false,
|
||||
toc: [
|
||||
// QUASAR
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
// STORES
|
||||
|
||||
const pageStore = usePageStore()
|
||||
const siteStore = useSiteStore()
|
||||
|
||||
// ROUTER
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// META
|
||||
|
||||
useMeta({
|
||||
title: pageStore.title
|
||||
})
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
showSideDialog: false,
|
||||
sideDialogComponent: null,
|
||||
showGlobalDialog: false,
|
||||
globalDialogComponent: null,
|
||||
showTagsEditBtn: false,
|
||||
tagEditMode: false,
|
||||
toc: [
|
||||
{
|
||||
key: 'h1-0',
|
||||
label: 'Introduction'
|
||||
},
|
||||
{
|
||||
key: 'h1-1',
|
||||
label: 'Planets',
|
||||
children: [
|
||||
{
|
||||
key: 'h1-0',
|
||||
label: 'Introduction'
|
||||
},
|
||||
{
|
||||
key: 'h1-1',
|
||||
label: 'Planets',
|
||||
key: 'h2-0',
|
||||
label: 'Earth',
|
||||
children: [
|
||||
{
|
||||
key: 'h2-0',
|
||||
label: 'Earth',
|
||||
key: 'h3-0',
|
||||
label: 'Countries',
|
||||
children: [
|
||||
{
|
||||
key: 'h3-0',
|
||||
label: 'Countries',
|
||||
key: 'h4-0',
|
||||
label: 'Cities',
|
||||
children: [
|
||||
{
|
||||
key: 'h4-0',
|
||||
label: 'Cities',
|
||||
key: 'h5-0',
|
||||
label: 'Montreal',
|
||||
children: [
|
||||
{
|
||||
key: 'h5-0',
|
||||
label: 'Montreal',
|
||||
children: [
|
||||
{
|
||||
key: 'h6-0',
|
||||
label: 'Districts'
|
||||
}
|
||||
]
|
||||
key: 'h6-0',
|
||||
label: 'Districts'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'h2-1',
|
||||
label: 'Mars'
|
||||
},
|
||||
{
|
||||
key: 'h2-2',
|
||||
label: 'Jupiter'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'h2-1',
|
||||
label: 'Mars'
|
||||
},
|
||||
{
|
||||
key: 'h2-2',
|
||||
label: 'Jupiter'
|
||||
}
|
||||
],
|
||||
tocExpanded: ['h1-0', 'h1-1'],
|
||||
tocSelected: [],
|
||||
currentRating: 3,
|
||||
thumbStyle: {
|
||||
right: '2px',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#000',
|
||||
width: '5px',
|
||||
opacity: 0.15
|
||||
},
|
||||
barStyle: {
|
||||
backgroundColor: '#FAFAFA',
|
||||
width: '9px',
|
||||
opacity: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mode: sync('page/mode', false),
|
||||
editorMode: get('page/editorMode', false),
|
||||
breadcrumbs: get('page/breadcrumbs', false),
|
||||
title: sync('page/title', false),
|
||||
description: sync('page/description', false),
|
||||
relations: get('page/relations', false),
|
||||
tags: sync('page/tags', false),
|
||||
ratingsMode: get('site/ratingsMode', false),
|
||||
allowComments: get('page/allowComments', false),
|
||||
allowContributions: get('page/allowContributions', false),
|
||||
allowRatings: get('page/allowRatings', false),
|
||||
showSidebar () {
|
||||
return this.$store.get('page/showSidebar') && this.$store.get('site/showSidebar')
|
||||
},
|
||||
showTags: get('page/showTags', false),
|
||||
showToc: get('page/showToc', false),
|
||||
tocDepth: get('page/tocDepth', false),
|
||||
isPublished: get('page/isPublished', false),
|
||||
pageIcon: sync('page/icon', false),
|
||||
render: get('page/render', false),
|
||||
editorComponent () {
|
||||
return this.$store.get('page/editor') ? `editor-${this.$store.get('page/editor')}` : null
|
||||
},
|
||||
relationsLeft () {
|
||||
return this.relations ? this.relations.filter(r => r.position === 'left') : []
|
||||
},
|
||||
relationsCenter () {
|
||||
return this.relations ? this.relations.filter(r => r.position === 'center') : []
|
||||
},
|
||||
relationsRight () {
|
||||
return this.relations ? this.relations.filter(r => r.position === 'right') : []
|
||||
},
|
||||
editMode () {
|
||||
return this.mode === 'edit'
|
||||
},
|
||||
editCreateMode () {
|
||||
return this.mode === 'edit' && this.editorMode === 'create'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
toc () {
|
||||
this.refreshTocExpanded()
|
||||
},
|
||||
tocDepth () {
|
||||
this.refreshTocExpanded()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.refreshTocExpanded()
|
||||
},
|
||||
methods: {
|
||||
togglePageProperties () {
|
||||
this.sideDialogComponent = 'PagePropertiesDialog'
|
||||
this.showSideDialog = true
|
||||
},
|
||||
togglePageData () {
|
||||
this.sideDialogComponent = 'PageDataDialog'
|
||||
this.showSideDialog = true
|
||||
},
|
||||
savePage () {
|
||||
this.globalDialogComponent = 'PageSaveDialog'
|
||||
this.showGlobalDialog = true
|
||||
},
|
||||
refreshTocExpanded (baseToc) {
|
||||
const toExpand = []
|
||||
let isRootNode = false
|
||||
if (!baseToc) {
|
||||
baseToc = this.toc
|
||||
isRootNode = true
|
||||
],
|
||||
tocExpanded: ['h1-0', 'h1-1'],
|
||||
tocSelected: [],
|
||||
currentRating: 3
|
||||
})
|
||||
const thumbStyle = {
|
||||
right: '2px',
|
||||
borderRadius: '5px',
|
||||
backgroundColor: '#000',
|
||||
width: '5px',
|
||||
opacity: 0.15
|
||||
}
|
||||
const barStyle = {
|
||||
backgroundColor: '#FAFAFA',
|
||||
width: '9px',
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const showSidebar = computed(() => {
|
||||
return pageStore.showSidebar && siteStore.showSidebar
|
||||
})
|
||||
const editorComponent = computed(() => {
|
||||
return pageStore.editor ? `editor-${pageStore.editor}` : null
|
||||
})
|
||||
const relationsLeft = computed(() => {
|
||||
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'left') : []
|
||||
})
|
||||
const relationsCenter = computed(() => {
|
||||
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'center') : []
|
||||
})
|
||||
const relationsRight = computed(() => {
|
||||
return pageStore.relations ? pageStore.relations.filter(r => r.position === 'right') : []
|
||||
})
|
||||
const editMode = computed(() => {
|
||||
return pageStore.mode === 'edit'
|
||||
})
|
||||
const editCreateMode = computed(() => {
|
||||
return pageStore.mode === 'edit' && pageStore.mode === 'create'
|
||||
})
|
||||
const editUrl = computed(() => {
|
||||
let pagePath = siteStore.useLocales ? `${pageStore.locale}/` : ''
|
||||
pagePath += !pageStore.path ? 'home' : pageStore.path
|
||||
return `/_edit/${pagePath}`
|
||||
})
|
||||
|
||||
// WATCHERS
|
||||
|
||||
watch(() => state.toc, refreshTocExpanded)
|
||||
watch(() => pageStore.tocDepth, refreshTocExpanded)
|
||||
|
||||
// METHODS
|
||||
|
||||
function getFullPath ({ locale, path }) {
|
||||
if (siteStore.useLocales) {
|
||||
return `/${locale}/${path}`
|
||||
} else {
|
||||
return `/${path}`
|
||||
}
|
||||
}
|
||||
|
||||
function togglePageProperties () {
|
||||
state.sideDialogComponent = 'PagePropertiesDialog'
|
||||
state.showSideDialog = true
|
||||
}
|
||||
|
||||
function togglePageData () {
|
||||
state.sideDialogComponent = 'PageDataDialog'
|
||||
state.showSideDialog = true
|
||||
}
|
||||
|
||||
function savePage () {
|
||||
state.globalDialogComponent = 'PageSaveDialog'
|
||||
state.showGlobalDialog = true
|
||||
}
|
||||
|
||||
function refreshTocExpanded (baseToc) {
|
||||
const toExpand = []
|
||||
let isRootNode = false
|
||||
if (!baseToc) {
|
||||
baseToc = state.toc
|
||||
isRootNode = true
|
||||
}
|
||||
if (baseToc.length > 0) {
|
||||
for (const node of baseToc) {
|
||||
if (node.key >= `h${pageStore.tocDepth.min}` && node.key <= `h${pageStore.tocDepth.max}`) {
|
||||
toExpand.push(node.key)
|
||||
}
|
||||
if (baseToc.length > 0) {
|
||||
for (const node of baseToc) {
|
||||
if (node.key >= `h${this.tocDepth.min}` && node.key <= `h${this.tocDepth.max}`) {
|
||||
toExpand.push(node.key)
|
||||
}
|
||||
if (node.children?.length && node.key < `h${this.tocDepth.max}`) {
|
||||
toExpand.push(...this.refreshTocExpanded(node.children))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRootNode) {
|
||||
this.tocExpanded = toExpand
|
||||
} else {
|
||||
return toExpand
|
||||
if (node.children?.length && node.key < `h${pageStore.tocDepth.max}`) {
|
||||
toExpand.push(...refreshTocExpanded(node.children))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRootNode) {
|
||||
state.tocExpanded = toExpand
|
||||
} else {
|
||||
return toExpand
|
||||
}
|
||||
}
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
refreshTocExpanded()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
+13
-10
@@ -1,13 +1,13 @@
|
||||
|
||||
const routes = [
|
||||
// {
|
||||
// path: '/',
|
||||
// component: () => import('../layouts/MainLayout.vue'),
|
||||
// children: [
|
||||
// { path: '', component: () => import('../pages/Index.vue') },
|
||||
// { path: 'n/:editor?', component: () => import('../pages/Index.vue') }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('../layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{ path: '', component: () => import('../pages/Index.vue') }
|
||||
// { path: 'n/:editor?', component: () => import('../pages/Index.vue') }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('layouts/AuthLayout.vue'),
|
||||
@@ -36,7 +36,6 @@ const routes = [
|
||||
{ path: ':siteid/locale', component: () => import('pages/AdminLocale.vue') },
|
||||
{ path: ':siteid/login', component: () => import('pages/AdminLogin.vue') },
|
||||
{ path: ':siteid/navigation', component: () => import('pages/AdminNavigation.vue') },
|
||||
// { path: ':siteid/rendering', component: () => import('pages/AdminRendering.vue') },
|
||||
{ path: ':siteid/storage/:id?', component: () => import('pages/AdminStorage.vue') },
|
||||
{ path: ':siteid/theme', component: () => import('pages/AdminTheme.vue') },
|
||||
// -> Users
|
||||
@@ -48,6 +47,7 @@ const routes = [
|
||||
{ path: 'extensions', component: () => import('pages/AdminExtensions.vue') },
|
||||
{ path: 'instances', component: () => import('pages/AdminInstances.vue') },
|
||||
{ path: 'mail', component: () => import('pages/AdminMail.vue') },
|
||||
// { path: 'rendering', component: () => import('pages/AdminRendering.vue') },
|
||||
{ path: 'scheduler', component: () => import('pages/AdminScheduler.vue') },
|
||||
{ path: 'security', component: () => import('pages/AdminSecurity.vue') },
|
||||
{ path: 'system', component: () => import('pages/AdminSystem.vue') },
|
||||
@@ -74,7 +74,10 @@ const routes = [
|
||||
// but you can also remove it
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
component: () => import('pages/ErrorNotFound.vue')
|
||||
component: () => import('../layouts/MainLayout.vue'),
|
||||
children: [
|
||||
{ path: '', component: () => import('../pages/Index.vue') }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user