Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
W
wiki-js
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
1
Issues
1
List
Board
Labels
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
wiki-js
Commits
7128b160
Unverified
Commit
7128b160
authored
Oct 28, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: rendering + new page view
parent
055fcc6b
Hide whitespace changes
Inline
Side-by-side
Showing
48 changed files
with
957 additions
and
573 deletions
+957
-573
data.yml
server/app/data.yml
+8
-1
common.js
server/controllers/common.js
+16
-15
db.js
server/core/db.js
+8
-4
scheduler.js
server/core/scheduler.js
+72
-11
3.0.0.js
server/db/migrations/3.0.0.js
+1
-4
system.js
server/graph/resolvers/system.js
+52
-0
system.graphql
server/graph/schemas/system.graphql
+8
-0
common.js
server/helpers/common.js
+28
-0
editors.js
server/models/editors.js
+0
-41
pages.js
server/models/pages.js
+27
-18
renderers.js
server/models/renderers.js
+51
-5
definition.yml
server/modules/rendering/html-asciinema/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-blockquotes/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-codehighlighter/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-core/definition.yml
+1
-1
renderer.js
server/modules/rendering/html-core/renderer.js
+5
-8
definition.yml
server/modules/rendering/html-diagram/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-image-prefetch/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-mediaplayers/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-mermaid/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-security/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-tabset/definition.yml
+1
-1
definition.yml
server/modules/rendering/html-twemoji/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-abbr/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-core/definition.yml
+1
-1
renderer.js
server/modules/rendering/markdown-core/renderer.js
+1
-1
definition.yml
server/modules/rendering/markdown-emoji/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-expandtabs/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-footnotes/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-imsize/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-katex/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-kroki/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-mathjax/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-multi-table/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-plantuml/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-supsub/definition.yml
+1
-1
definition.yml
server/modules/rendering/markdown-tasklists/definition.yml
+1
-1
render-page.js
server/tasks/workers/render-page.js
+92
-0
page.pug
server/views/page.pug
+0
-13
worker.js
server/worker.js
+17
-1
PageTags.vue
ux/src/components/PageTags.vue
+43
-28
SocialSharingMenu.vue
ux/src/components/SocialSharingMenu.vue
+107
-86
en.json
ux/src/i18n/locales/en.json
+5
-1
AdminLayout.vue
ux/src/layouts/AdminLayout.vue
+5
-5
MainLayout.vue
ux/src/layouts/MainLayout.vue
+56
-38
AdminScheduler.vue
ux/src/pages/AdminScheduler.vue
+111
-3
Index.vue
ux/src/pages/Index.vue
+207
-256
routes.js
ux/src/router/routes.js
+13
-10
No files found.
server/app/data.yml
View file @
7128b160
...
...
@@ -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'
...
...
server/controllers/common.js
View file @
7128b160
...
...
@@ -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
{
...
...
server/core/db.js
View file @
7128b160
...
...
@@ -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
()
...
...
server/core/scheduler.js
View file @
7128b160
...
...
@@ -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
...
...
server/db/migrations/3.0.0.js
View file @
7128b160
...
...
@@ -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'
)
})
...
...
server/graph/resolvers/system.js
View file @
7128b160
...
...
@@ -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
)
...
...
server/graph/schemas/system.graphql
View file @
7128b160
...
...
@@ -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
...
...
server/helpers/common.js
View file @
7128b160
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
*
...
...
server/models/editors.js
deleted
100644 → 0
View file @
055fcc6b
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'
}
}
}
server/models/pages.js
View file @
7128b160
...
...
@@ -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
()
}))
}
...
...
server/models/renderers.js
View file @
7128b160
...
...
@@ -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
...
...
server/modules/rendering/html-asciinema/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Embed asciinema players from compatible links
author
:
requarks.io
icon
:
mdi-theater
enabledDefault
:
false
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
server/modules/rendering/html-blockquotes/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Parse blockquotes box styling
author
:
requarks.io
icon
:
mdi-alpha-t-box-outline
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
server/modules/rendering/html-codehighlighter/definition.yml
View file @
7128b160
...
...
@@ -4,6 +4,6 @@ description: Syntax detector for programming code
author
:
requarks.io
icon
:
mdi-code-braces
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
step
:
pre
props
:
{}
server/modules/rendering/html-core/definition.yml
View file @
7128b160
key
:
html
C
ore
key
:
html
-c
ore
title
:
Core
description
:
Basic HTML Parser
author
:
requarks.io
...
...
server/modules/rendering/html-core/renderer.js
View file @
7128b160
...
...
@@ -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
.
n
amespacing
)
{
if
(
this
.
site
.
config
.
localeN
amespacing
)
{
// -> Reformat paths
if
(
href
.
indexOf
(
'/'
)
!==
0
)
{
if
(
this
.
config
.
absoluteLinks
)
{
...
...
server/modules/rendering/html-diagram/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: HTML Processing for diagrams (draw.io)
author
:
requarks.io
icon
:
mdi-chart-multiline
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
server/modules/rendering/html-image-prefetch/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Prefetch remotely rendered images (korki/plantuml)
author
:
requarks.io
icon
:
mdi-cloud-download-outline
enabledDefault
:
false
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
server/modules/rendering/html-mediaplayers/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Embed players such as Youtube, Vimeo, Soundcloud, etc.
author
:
requarks.io
icon
:
mdi-video
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
server/modules/rendering/html-mermaid/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Generate flowcharts from Mermaid syntax
author
:
requarks.io
icon
:
mdi-arrow-decision-outline
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
server/modules/rendering/html-security/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: Filter and strips potentially dangerous content
author
:
requarks.io
icon
:
mdi-fire
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
step
:
post
order
:
99999
props
:
...
...
server/modules/rendering/html-tabset/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Transform headers into tabs
author
:
requarks.io
icon
:
mdi-tab
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
server/modules/rendering/html-twemoji/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: Apply Twitter Emojis to all Unicode emojis
author
:
requarks.io
icon
:
mdi-emoticon-happy-outline
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
step
:
post
order
:
10
props
:
{}
server/modules/rendering/markdown-abbr/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Parse abbreviations into abbr tags
author
:
requarks.io
icon
:
mdi-contain-start
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
server/modules/rendering/markdown-core/definition.yml
View file @
7128b160
key
:
markdown
C
ore
key
:
markdown
-c
ore
title
:
Core
description
:
Basic Markdown Parser
author
:
requarks.io
...
...
server/modules/rendering/markdown-core/renderer.js
View file @
7128b160
...
...
@@ -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
)
}
...
...
server/modules/rendering/markdown-emoji/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Convert tags to emojis
author
:
requarks.io
icon
:
mdi-sticker-emoji
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
server/modules/rendering/markdown-expandtabs/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: Replace tabs with spaces in code blocks
author
:
requarks.io
icon
:
mdi-arrow-expand-horizontal
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
tabWidth
:
type
:
Number
...
...
server/modules/rendering/markdown-footnotes/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Parse footnotes references
author
:
requarks.io
icon
:
mdi-page-layout-footer
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
server/modules/rendering/markdown-imsize/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Adds dimensions attributes to images
author
:
requarks.io
icon
:
mdi-image-size-select-large
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
server/modules/rendering/markdown-katex/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: LaTeX Math + Chemical Expression Typesetting Renderer
author
:
requarks.io
icon
:
mdi-math-integral
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
useInline
:
type
:
Boolean
...
...
server/modules/rendering/markdown-kroki/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: Kroki Diagrams Parser
author
:
rlanyi (based on PlantUML renderer)
icon
:
mdi-sitemap
enabledDefault
:
false
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
server
:
type
:
String
...
...
server/modules/rendering/markdown-mathjax/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: LaTeX Math + Chemical Expression Typesetting Renderer
author
:
requarks.io
icon
:
mdi-math-integral
enabledDefault
:
false
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
useInline
:
type
:
Boolean
...
...
server/modules/rendering/markdown-multi-table/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: Add MultiMarkdown table support
author
:
requarks.io
icon
:
mdi-table
enabledDefault
:
false
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
multilineEnabled
:
type
:
Boolean
...
...
server/modules/rendering/markdown-plantuml/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: PlantUML Markdown Parser
author
:
ethanmdavidson
icon
:
mdi-sitemap
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
server
:
type
:
String
...
...
server/modules/rendering/markdown-supsub/definition.yml
View file @
7128b160
...
...
@@ -4,7 +4,7 @@ description: Parse subscript and superscript tags
author
:
requarks.io
icon
:
mdi-format-superscript
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
subEnabled
:
type
:
Boolean
...
...
server/modules/rendering/markdown-tasklists/definition.yml
View file @
7128b160
...
...
@@ -4,5 +4,5 @@ description: Parse task lists to checkboxes
author
:
requarks.io
icon
:
mdi-format-list-checks
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
server/tasks/workers/render-page.js
0 → 100644
View file @
7128b160
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
}
}
server/views/page.pug
View file @
7128b160
...
...
@@ -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
server/worker.js
View file @
7128b160
...
...
@@ -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
...
...
ux/src/components/PageTags.vue
View file @
7128b160
<
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'
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
)
}
<
script
setup
>
import
{
useQuasar
}
from
'quasar'
import
{
reactive
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
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
>
ux/src/components/SocialSharingMenu.vue
View file @
7128b160
...
...
@@ -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
:
''
}
},
data
()
{
return
{
width
:
626
,
height
:
436
,
left
:
0
,
top
:
0
,
clip
:
null
}
},
computed
:
{
urlFormatted
()
{
if
(
!
import
.
meta
.
env
.
SSR
)
{
return
this
.
url
?
this
.
url
:
window
.
location
.
href
}
else
{
return
''
}
}
// PROPS
const
props
=
defineProps
({
url
:
{
type
:
String
,
default
:
null
},
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
()
}
title
:
{
type
:
String
,
default
:
'Untitled Page'
},
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
>
ux/src/i18n/locales/en.json
View file @
7128b160
...
...
@@ -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."
}
ux/src/layouts/AdminLayout.vue
View file @
7128b160
...
...
@@ -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')
...
...
ux/src/layouts/MainLayout.vue
View file @
7128b160
...
...
@@ -2,7 +2,7 @@
q-layout(view='hHh Lpr lff')
header-nav
q-drawer.bg-sidebar(
v-model='showSideNav'
v-model='s
iteStore.s
howSideNav'
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"
>
...
...
ux/src/pages/AdminScheduler.vue
View file @
7128b160
...
...
@@ -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
(()
=>
{
...
...
ux/src/pages/Index.vue
View file @
7128b160
...
...
@@ -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='page
I
con'
:name='page
Store.i
con'
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='s
tate.s
howTagsEditBtn = true'
@mouseleave='s
tate.s
howTagsEditBtn = 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='s
tate.s
howTagsEditBtn'
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='s
tate.s
howSideDialog'
position='right'
full-height
transition-show='jump-left'
transition-hide='jump-right'
class='floating-sidepanel'
)
component(:is='sideDialogComponent')
component(:is='s
tate.s
ideDialogComponent')
q-dialog(
v-model='showGlobalDialog'
v-model='s
tate.s
howGlobalDialog'
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
:
[
{
key
:
'h1-0'
,
label
:
'Introduction'
},
// 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
:
'h
1-1
'
,
label
:
'
Planets
'
,
key
:
'h
2-0
'
,
label
:
'
Earth
'
,
children
:
[
{
key
:
'h
2
-0'
,
label
:
'
Earth
'
,
key
:
'h
3
-0'
,
label
:
'
Countries
'
,
children
:
[
{
key
:
'h
3
-0'
,
label
:
'C
ountr
ies'
,
key
:
'h
4
-0'
,
label
:
'C
it
ies'
,
children
:
[
{
key
:
'h
4
-0'
,
label
:
'
Cities
'
,
key
:
'h
5
-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
}
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
))
}
}
],
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
(
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"
>
...
...
ux/src/router/routes.js
View file @
7128b160
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'
)
}
]
}
]
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment