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
You need to sign in or sign up before continuing.
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:
...
@@ -33,7 +33,7 @@ defaults:
workers
:
3
workers
:
3
pollingCheck
:
5
pollingCheck
:
5
scheduledCheck
:
300
scheduledCheck
:
300
maxRetries
:
5
maxRetries
:
2
retryBackoff
:
60
retryBackoff
:
60
historyExpiration
:
90000
historyExpiration
:
90000
# DB defaults
# DB defaults
...
@@ -83,6 +83,13 @@ defaults:
...
@@ -83,6 +83,13 @@ defaults:
search
:
search
:
maxHits
:
100
maxHits
:
100
maintainerEmail
:
security@requarks.io
maintainerEmail
:
security@requarks.io
editors
:
code
:
contentType
:
html
markdown
:
contentType
:
markdown
wysiwyg
:
contentType
:
html
groups
:
groups
:
defaultPermissions
:
defaultPermissions
:
-
'
read:pages'
-
'
read:pages'
...
...
server/controllers/common.js
View file @
7128b160
...
@@ -528,9 +528,9 @@ router.get('/*', async (req, res, next) => {
...
@@ -528,9 +528,9 @@ router.get('/*', async (req, res, next) => {
// -> Build theme code injection
// -> Build theme code injection
const
injectCode
=
{
const
injectCode
=
{
css
:
WIKI
.
config
.
theming
.
injectCSS
,
css
:
''
,
//
WIKI.config.theming.injectCSS,
head
:
WIKI
.
config
.
theming
.
injectHead
,
head
:
''
,
//
WIKI.config.theming.injectHead,
body
:
WIKI
.
config
.
theming
.
injectBody
body
:
''
//
WIKI.config.theming.injectBody
}
}
// Handle missing extra field
// Handle missing extra field
...
@@ -551,12 +551,12 @@ router.get('/*', async (req, res, next) => {
...
@@ -551,12 +551,12 @@ router.get('/*', async (req, res, next) => {
// -> Inject comments variables
// -> Inject comments variables
const
commentTmpl
=
{
const
commentTmpl
=
{
codeTemplate
:
WIKI
.
data
.
commentProvider
.
codeTemplate
,
codeTemplate
:
''
,
//
WIKI.data.commentProvider.codeTemplate,
head
:
WIKI
.
data
.
commentProvider
.
head
,
head
:
''
,
//
WIKI.data.commentProvider.head,
body
:
WIKI
.
data
.
commentProvider
.
body
,
body
:
''
,
//
WIKI.data.commentProvider.body,
main
:
WIKI
.
data
.
commentProvider
.
main
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
:
'pageUrl'
,
value
:
`
${
WIKI
.
config
.
host
}
/i/
${
page
.
id
}
`
},
{
key
:
'pageId'
,
value
:
page
.
id
}
{
key
:
'pageId'
,
value
:
page
.
id
}
...
@@ -568,13 +568,14 @@ router.get('/*', async (req, res, next) => {
...
@@ -568,13 +568,14 @@ router.get('/*', async (req, res, next) => {
}
}
// -> Render view
// -> Render view
res
.
render
(
'page'
,
{
res
.
sendFile
(
path
.
join
(
WIKI
.
ROOTPATH
,
'assets/index.html'
))
page
,
// res.render('page', {
sidebar
,
// page,
injectCode
,
// sidebar,
comments
:
commentTmpl
,
// injectCode,
effectivePermissions
// comments: commentTmpl,
})
// effectivePermissions
// })
}
else
if
(
pageArgs
.
path
===
'home'
)
{
}
else
if
(
pageArgs
.
path
===
'home'
)
{
res
.
redirect
(
'/_welcome'
)
res
.
redirect
(
'/_welcome'
)
}
else
{
}
else
{
...
...
server/core/db.js
View file @
7128b160
...
@@ -21,7 +21,7 @@ module.exports = {
...
@@ -21,7 +21,7 @@ module.exports = {
/**
/**
* Initialize DB
* Initialize DB
*/
*/
init
()
{
init
(
workerMode
=
false
)
{
let
self
=
this
let
self
=
this
WIKI
.
logger
.
info
(
'Checking DB configuration...'
)
WIKI
.
logger
.
info
(
'Checking DB configuration...'
)
...
@@ -85,10 +85,14 @@ module.exports = {
...
@@ -85,10 +85,14 @@ module.exports = {
connection
:
this
.
config
,
connection
:
this
.
config
,
searchPath
:
[
WIKI
.
config
.
db
.
schemas
.
wiki
],
searchPath
:
[
WIKI
.
config
.
db
.
schemas
.
wiki
],
pool
:
{
pool
:
{
...
WIKI
.
config
.
pool
,
...
workerMode
?
{
min
:
0
,
max
:
1
}
:
WIKI
.
config
.
pool
,
async
afterCreate
(
conn
,
done
)
{
async
afterCreate
(
conn
,
done
)
{
// -> Set Connection App Name
// -> 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
()
done
()
}
}
},
},
...
@@ -145,7 +149,7 @@ module.exports = {
...
@@ -145,7 +149,7 @@ module.exports = {
// Perform init tasks
// Perform init tasks
this
.
onReady
=
(
async
()
=>
{
this
.
onReady
=
workerMode
?
Promise
.
resolve
()
:
(
async
()
=>
{
await
initTasks
.
connect
()
await
initTasks
.
connect
()
await
initTasks
.
migrateFromLegacy
()
await
initTasks
.
migrateFromLegacy
()
await
initTasks
.
syncSchemas
()
await
initTasks
.
syncSchemas
()
...
...
server/core/scheduler.js
View file @
7128b160
...
@@ -4,6 +4,9 @@ const autoload = require('auto-load')
...
@@ -4,6 +4,9 @@ const autoload = require('auto-load')
const
path
=
require
(
'node:path'
)
const
path
=
require
(
'node:path'
)
const
cronparser
=
require
(
'cron-parser'
)
const
cronparser
=
require
(
'cron-parser'
)
const
{
DateTime
}
=
require
(
'luxon'
)
const
{
DateTime
}
=
require
(
'luxon'
)
const
{
v4
:
uuid
}
=
require
(
'uuid'
)
const
{
createDeferred
}
=
require
(
'../helpers/common'
)
const
_
=
require
(
'lodash'
)
module
.
exports
=
{
module
.
exports
=
{
workerPool
:
null
,
workerPool
:
null
,
...
@@ -12,6 +15,7 @@ module.exports = {
...
@@ -12,6 +15,7 @@ module.exports = {
pollingRef
:
null
,
pollingRef
:
null
,
scheduledRef
:
null
,
scheduledRef
:
null
,
tasks
:
null
,
tasks
:
null
,
completionPromises
:
[],
async
init
()
{
async
init
()
{
this
.
maxWorkers
=
WIKI
.
config
.
scheduler
.
workers
===
'auto'
?
(
os
.
cpus
().
length
-
1
)
:
WIKI
.
config
.
scheduler
.
workers
this
.
maxWorkers
=
WIKI
.
config
.
scheduler
.
workers
===
'auto'
?
(
os
.
cpus
().
length
-
1
)
:
WIKI
.
config
.
scheduler
.
workers
if
(
this
.
maxWorkers
<
1
)
{
this
.
maxWorkers
=
1
}
if
(
this
.
maxWorkers
<
1
)
{
this
.
maxWorkers
=
1
}
...
@@ -38,6 +42,20 @@ module.exports = {
...
@@ -38,6 +42,20 @@ module.exports = {
}
}
break
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 = {
...
@@ -56,23 +74,52 @@ module.exports = {
WIKI
.
logger
.
info
(
'Scheduler: [ STARTED ]'
)
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
{
try
{
await
WIKI
.
db
.
knex
(
'jobs'
).
insert
({
const
jobId
=
uuid
()
task
,
const
jobDefer
=
createDeferred
()
useWorker
:
!
(
typeof
this
.
tasks
[
task
]
===
'function'
),
if
(
promise
)
{
payload
,
this
.
completionPromises
.
push
({
maxRetries
:
maxRetries
??
WIKI
.
config
.
scheduler
.
maxRetries
,
id
:
jobId
,
isScheduled
,
added
:
DateTime
.
utc
(),
waitUntil
,
resolve
:
jobDefer
.
resolve
,
createdBy
:
WIKI
.
INSTANCE_ID
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
)
{
if
(
notify
)
{
WIKI
.
db
.
listener
.
publish
(
'scheduler'
,
{
WIKI
.
db
.
listener
.
publish
(
'scheduler'
,
{
source
:
WIKI
.
INSTANCE_ID
,
source
:
WIKI
.
INSTANCE_ID
,
event
:
'newJob'
event
:
'newJob'
,
id
:
jobId
})
})
}
}
return
{
id
:
jobId
,
...
promise
&&
{
promise
:
jobDefer
.
promise
}
}
}
catch
(
err
)
{
}
catch
(
err
)
{
WIKI
.
logger
.
warn
(
`Failed to add job to scheduler:
${
err
.
message
}
`
)
WIKI
.
logger
.
warn
(
`Failed to add job to scheduler:
${
err
.
message
}
`
)
}
}
...
@@ -130,6 +177,12 @@ module.exports = {
...
@@ -130,6 +177,12 @@ module.exports = {
completedAt
:
new
Date
()
completedAt
:
new
Date
()
})
})
WIKI
.
logger
.
info
(
`Completed job
${
job
.
id
}
:
${
job
.
task
}
`
)
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
)
{
}
catch
(
err
)
{
WIKI
.
logger
.
warn
(
`Failed to complete job
${
job
.
id
}
:
${
job
.
task
}
[ FAILED ]`
)
WIKI
.
logger
.
warn
(
`Failed to complete job
${
job
.
id
}
:
${
job
.
task
}
[ FAILED ]`
)
WIKI
.
logger
.
warn
(
err
)
WIKI
.
logger
.
warn
(
err
)
...
@@ -137,9 +190,17 @@ module.exports = {
...
@@ -137,9 +190,17 @@ module.exports = {
await
WIKI
.
db
.
knex
(
'jobHistory'
).
where
({
await
WIKI
.
db
.
knex
(
'jobHistory'
).
where
({
id
:
job
.
id
id
:
job
.
id
}).
update
({
}).
update
({
attempt
:
job
.
retries
+
1
,
state
:
'failed'
,
state
:
'failed'
,
lastErrorMessage
:
err
.
message
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
// -> Reschedule for retry
if
(
job
.
retries
<
job
.
maxRetries
)
{
if
(
job
.
retries
<
job
.
maxRetries
)
{
const
backoffDelay
=
(
2
**
job
.
retries
)
*
WIKI
.
config
.
scheduler
.
retryBackoff
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 => {
...
@@ -243,7 +243,7 @@ exports.up = async knex => {
table
.
uuid
(
'id'
).
notNullable
().
primary
().
defaultTo
(
knex
.
raw
(
'gen_random_uuid()'
))
table
.
uuid
(
'id'
).
notNullable
().
primary
().
defaultTo
(
knex
.
raw
(
'gen_random_uuid()'
))
table
.
string
(
'module'
).
notNullable
()
table
.
string
(
'module'
).
notNullable
()
table
.
boolean
(
'isEnabled'
).
notNullable
().
defaultTo
(
false
)
table
.
boolean
(
'isEnabled'
).
notNullable
().
defaultTo
(
false
)
table
.
jsonb
(
'config'
)
table
.
jsonb
(
'config'
)
.
notNullable
().
defaultTo
(
'{}'
)
})
})
// SETTINGS ----------------------------
// SETTINGS ----------------------------
.
createTable
(
'settings'
,
table
=>
{
.
createTable
(
'settings'
,
table
=>
{
...
@@ -370,9 +370,6 @@ exports.up = async knex => {
...
@@ -370,9 +370,6 @@ exports.up = async knex => {
table
.
uuid
(
'pageId'
).
notNullable
().
references
(
'id'
).
inTable
(
'pages'
).
onDelete
(
'CASCADE'
)
table
.
uuid
(
'pageId'
).
notNullable
().
references
(
'id'
).
inTable
(
'pages'
).
onDelete
(
'CASCADE'
)
table
.
string
(
'localeCode'
,
5
).
references
(
'code'
).
inTable
(
'locales'
)
table
.
string
(
'localeCode'
,
5
).
references
(
'code'
).
inTable
(
'locales'
)
})
})
.
table
(
'renderers'
,
table
=>
{
table
.
uuid
(
'siteId'
).
notNullable
().
references
(
'id'
).
inTable
(
'sites'
)
})
.
table
(
'storage'
,
table
=>
{
.
table
(
'storage'
,
table
=>
{
table
.
uuid
(
'siteId'
).
notNullable
().
references
(
'id'
).
inTable
(
'sites'
)
table
.
uuid
(
'siteId'
).
notNullable
().
references
(
'id'
).
inTable
(
'sites'
)
})
})
...
...
server/graph/resolvers/system.js
View file @
7128b160
...
@@ -79,6 +79,25 @@ module.exports = {
...
@@ -79,6 +79,25 @@ module.exports = {
}
}
},
},
Mutation
:
{
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
)
{
async
disconnectWS
(
obj
,
args
,
context
)
{
WIKI
.
servers
.
ws
.
disconnectSockets
(
true
)
WIKI
.
servers
.
ws
.
disconnectSockets
(
true
)
WIKI
.
logger
.
info
(
'All active websocket connections have been terminated.'
)
WIKI
.
logger
.
info
(
'All active websocket connections have been terminated.'
)
...
@@ -97,6 +116,39 @@ module.exports = {
...
@@ -97,6 +116,39 @@ module.exports = {
return
graphHelper
.
generateError
(
err
)
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
)
{
async
updateSystemFlags
(
obj
,
args
,
context
)
{
WIKI
.
config
.
flags
=
_
.
transform
(
args
.
flags
,
(
result
,
row
)
=>
{
WIKI
.
config
.
flags
=
_
.
transform
(
args
.
flags
,
(
result
,
row
)
=>
{
_
.
set
(
result
,
row
.
key
,
row
.
value
)
_
.
set
(
result
,
row
.
key
,
row
.
value
)
...
...
server/graph/schemas/system.graphql
View file @
7128b160
...
@@ -16,12 +16,20 @@ extend type Query {
...
@@ -16,12 +16,20 @@ extend type Query {
}
}
extend
type
Mutation
{
extend
type
Mutation
{
cancelJob
(
id
:
UUID
!
):
DefaultResponse
disconnectWS
:
DefaultResponse
disconnectWS
:
DefaultResponse
installExtension
(
installExtension
(
key
:
String
!
key
:
String
!
):
DefaultResponse
):
DefaultResponse
retryJob
(
id
:
UUID
!
):
DefaultResponse
updateSystemFlags
(
updateSystemFlags
(
flags
:
[
SystemFlagInput
]!
flags
:
[
SystemFlagInput
]!
):
DefaultResponse
):
DefaultResponse
...
...
server/helpers/common.js
View file @
7128b160
const
_
=
require
(
'lodash'
)
const
_
=
require
(
'lodash'
)
module
.
exports
=
{
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
* 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 {
...
@@ -34,7 +34,7 @@ module.exports = class Page extends Model {
required
:
[
'path'
,
'title'
],
required
:
[
'path'
,
'title'
],
properties
:
{
properties
:
{
id
:
{
type
:
'
integer
'
},
id
:
{
type
:
'
string
'
},
path
:
{
type
:
'string'
},
path
:
{
type
:
'string'
},
hash
:
{
type
:
'string'
},
hash
:
{
type
:
'string'
},
title
:
{
type
:
'string'
},
title
:
{
type
:
'string'
},
...
@@ -44,7 +44,7 @@ module.exports = class Page extends Model {
...
@@ -44,7 +44,7 @@ module.exports = class Page extends Model {
publishEndDate
:
{
type
:
'string'
},
publishEndDate
:
{
type
:
'string'
},
content
:
{
type
:
'string'
},
content
:
{
type
:
'string'
},
contentType
:
{
type
:
'string'
},
contentType
:
{
type
:
'string'
},
siteId
:
{
type
:
'string'
},
createdAt
:
{
type
:
'string'
},
createdAt
:
{
type
:
'string'
},
updatedAt
:
{
type
:
'string'
}
updatedAt
:
{
type
:
'string'
}
}
}
...
@@ -125,11 +125,11 @@ module.exports = class Page extends Model {
...
@@ -125,11 +125,11 @@ module.exports = class Page extends Model {
*/
*/
static
get
cacheSchema
()
{
static
get
cacheSchema
()
{
return
new
JSBinType
({
return
new
JSBinType
({
id
:
'
uint
'
,
id
:
'
string
'
,
authorId
:
'
uint
'
,
authorId
:
'
string
'
,
authorName
:
'string'
,
authorName
:
'string'
,
createdAt
:
'string'
,
createdAt
:
'string'
,
creatorId
:
'
uint
'
,
creatorId
:
'
string
'
,
creatorName
:
'string'
,
creatorName
:
'string'
,
description
:
'string'
,
description
:
'string'
,
editor
:
'string'
,
editor
:
'string'
,
...
@@ -137,6 +137,7 @@ module.exports = class Page extends Model {
...
@@ -137,6 +137,7 @@ module.exports = class Page extends Model {
publishEndDate
:
'string'
,
publishEndDate
:
'string'
,
publishStartDate
:
'string'
,
publishStartDate
:
'string'
,
render
:
'string'
,
render
:
'string'
,
siteId
:
'string'
,
tags
:
[
tags
:
[
{
{
tag
:
'string'
tag
:
'string'
...
@@ -291,7 +292,7 @@ module.exports = class Page extends Model {
...
@@ -291,7 +292,7 @@ module.exports = class Page extends Model {
authorId
:
opts
.
user
.
id
,
authorId
:
opts
.
user
.
id
,
content
:
opts
.
content
,
content
:
opts
.
content
,
creatorId
:
opts
.
user
.
id
,
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
,
description
:
opts
.
description
,
editor
:
opts
.
editor
,
editor
:
opts
.
editor
,
hash
:
pageHelper
.
generateHash
({
path
:
opts
.
path
,
locale
:
opts
.
locale
}),
hash
:
pageHelper
.
generateHash
({
path
:
opts
.
path
,
locale
:
opts
.
locale
}),
...
@@ -322,6 +323,9 @@ module.exports = class Page extends Model {
...
@@ -322,6 +323,9 @@ module.exports = class Page extends Model {
// -> Render page to HTML
// -> Render page to HTML
await
WIKI
.
db
.
pages
.
renderPage
(
page
)
await
WIKI
.
db
.
pages
.
renderPage
(
page
)
return
page
// TODO: Handle remaining flow
// -> Rebuild page tree
// -> Rebuild page tree
await
WIKI
.
db
.
pages
.
rebuildTree
()
await
WIKI
.
db
.
pages
.
rebuildTree
()
...
@@ -922,12 +926,15 @@ module.exports = class Page extends Model {
...
@@ -922,12 +926,15 @@ module.exports = class Page extends Model {
* @returns {Promise} Promise with no value
* @returns {Promise} Promise with no value
*/
*/
static
async
renderPage
(
page
)
{
static
async
renderPage
(
page
)
{
const
renderJob
=
await
WIKI
.
scheduler
.
registerJob
({
const
renderJob
=
await
WIKI
.
scheduler
.
addJob
({
name
:
'render-page'
,
task
:
'render-page'
,
immediate
:
true
,
payload
:
{
worker
:
true
id
:
page
.
id
},
page
.
id
)
},
return
renderJob
.
finished
maxRetries
:
0
,
promise
:
true
})
return
renderJob
.
promise
}
}
/**
/**
...
@@ -963,7 +970,7 @@ module.exports = class Page extends Model {
...
@@ -963,7 +970,7 @@ module.exports = class Page extends Model {
* @returns {Promise} Promise of the Page Model Instance
* @returns {Promise} Promise of the Page Model Instance
*/
*/
static
async
getPageFromDb
(
opts
)
{
static
async
getPageFromDb
(
opts
)
{
const
queryModeID
=
_
.
isNumber
(
opts
)
const
queryModeID
=
typeof
opts
===
'string'
try
{
try
{
return
WIKI
.
db
.
pages
.
query
()
return
WIKI
.
db
.
pages
.
query
()
.
column
([
.
column
([
...
@@ -985,6 +992,7 @@ module.exports = class Page extends Model {
...
@@ -985,6 +992,7 @@ module.exports = class Page extends Model {
'pages.localeCode'
,
'pages.localeCode'
,
'pages.authorId'
,
'pages.authorId'
,
'pages.creatorId'
,
'pages.creatorId'
,
'pages.siteId'
,
'pages.extra'
,
'pages.extra'
,
{
{
authorName
:
'author.name'
,
authorName
:
'author.name'
,
...
@@ -1033,7 +1041,7 @@ module.exports = class Page extends Model {
...
@@ -1033,7 +1041,7 @@ module.exports = class Page extends Model {
id
:
page
.
id
,
id
:
page
.
id
,
authorId
:
page
.
authorId
,
authorId
:
page
.
authorId
,
authorName
:
page
.
authorName
,
authorName
:
page
.
authorName
,
createdAt
:
page
.
createdAt
,
createdAt
:
page
.
createdAt
.
toISOString
()
,
creatorId
:
page
.
creatorId
,
creatorId
:
page
.
creatorId
,
creatorName
:
page
.
creatorName
,
creatorName
:
page
.
creatorName
,
description
:
page
.
description
,
description
:
page
.
description
,
...
@@ -1042,14 +1050,15 @@ module.exports = class Page extends Model {
...
@@ -1042,14 +1050,15 @@ module.exports = class Page extends Model {
css
:
_
.
get
(
page
,
'extra.css'
,
''
),
css
:
_
.
get
(
page
,
'extra.css'
,
''
),
js
:
_
.
get
(
page
,
'extra.js'
,
''
)
js
:
_
.
get
(
page
,
'extra.js'
,
''
)
},
},
publishState
:
page
.
publishState
,
publishState
:
page
.
publishState
??
''
,
publishEndDate
:
page
.
publishEndDate
,
publishEndDate
:
page
.
publishEndDate
??
''
,
publishStartDate
:
page
.
publishStartDate
,
publishStartDate
:
page
.
publishStartDate
??
''
,
render
:
page
.
render
,
render
:
page
.
render
,
siteId
:
page
.
siteId
,
tags
:
page
.
tags
.
map
(
t
=>
_
.
pick
(
t
,
[
'tag'
])),
tags
:
page
.
tags
.
map
(
t
=>
_
.
pick
(
t
,
[
'tag'
])),
title
:
page
.
title
,
title
:
page
.
title
,
toc
:
_
.
isString
(
page
.
toc
)
?
page
.
toc
:
JSON
.
stringify
(
page
.
toc
),
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 {
...
@@ -55,19 +55,65 @@ module.exports = class Renderer extends Model {
}
}
static
async
refreshRenderersFromDisk
()
{
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
if
(
updatedRenderers
>
0
)
{
await
WIKI
.
db
.
renderers
.
fetchDefinitions
()
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
)
{
static
async
getRenderingPipeline
(
contentType
)
{
const
renderersDb
=
await
WIKI
.
db
.
renderers
.
query
().
where
(
'isEnabled'
,
true
)
const
renderersDb
=
await
WIKI
.
db
.
renderers
.
query
().
where
(
'isEnabled'
,
true
)
if
(
renderersDb
&&
renderersDb
.
length
>
0
)
{
if
(
renderersDb
&&
renderersDb
.
length
>
0
)
{
const
renderers
=
renderersDb
.
map
(
rdr
=>
{
const
renderers
=
renderersDb
.
map
(
rdr
=>
{
const
renderer
=
_
.
find
(
WIKI
.
data
.
renderers
,
[
'key'
,
rdr
.
key
])
const
renderer
=
_
.
find
(
WIKI
.
data
.
renderers
,
[
'key'
,
rdr
.
module
])
return
{
return
{
...
renderer
,
...
renderer
,
config
:
rdr
.
config
config
:
rdr
.
config
...
...
server/modules/rendering/html-asciinema/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Embed asciinema players from compatible links
...
@@ -4,5 +4,5 @@ description: Embed asciinema players from compatible links
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-theater
icon
:
mdi-theater
enabledDefault
:
false
enabledDefault
:
false
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/html-blockquotes/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Parse blockquotes box styling
...
@@ -4,5 +4,5 @@ description: Parse blockquotes box styling
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-alpha-t-box-outline
icon
:
mdi-alpha-t-box-outline
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/html-codehighlighter/definition.yml
View file @
7128b160
...
@@ -4,6 +4,6 @@ description: Syntax detector for programming code
...
@@ -4,6 +4,6 @@ description: Syntax detector for programming code
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-code-braces
icon
:
mdi-code-braces
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
step
:
pre
step
:
pre
props
:
{}
props
:
{}
server/modules/rendering/html-core/definition.yml
View file @
7128b160
key
:
html
C
ore
key
:
html
-c
ore
title
:
Core
title
:
Core
description
:
Basic HTML Parser
description
:
Basic HTML Parser
author
:
requarks.io
author
:
requarks.io
...
...
server/modules/rendering/html-core/renderer.js
View file @
7128b160
...
@@ -21,7 +21,7 @@ module.exports = {
...
@@ -21,7 +21,7 @@ module.exports = {
// --------------------------------
// --------------------------------
for
(
let
child
of
_
.
reject
(
this
.
children
,
[
'step'
,
'post'
]))
{
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
)
await
renderer
.
init
(
$
,
child
.
config
)
}
}
...
@@ -33,10 +33,7 @@ module.exports = {
...
@@ -33,10 +33,7 @@ module.exports = {
const
reservedPrefixes
=
/^
\/[
a-z
]\/
/i
const
reservedPrefixes
=
/^
\/[
a-z
]\/
/i
const
exactReservedPaths
=
/^
\/[
a-z
]
$/i
const
exactReservedPaths
=
/^
\/[
a-z
]
$/i
const
isHostSet
=
WIKI
.
config
.
host
.
length
>
7
&&
WIKI
.
config
.
host
!==
'http://'
const
hasHostname
=
this
.
site
.
hostname
!==
'*'
if
(
!
isHostSet
)
{
WIKI
.
logger
.
warn
(
'Host is not set. You must set the Site Host under General in the Administration Area!'
)
}
$
(
'a'
).
each
((
i
,
elm
)
=>
{
$
(
'a'
).
each
((
i
,
elm
)
=>
{
let
href
=
$
(
elm
).
attr
(
'href'
)
let
href
=
$
(
elm
).
attr
(
'href'
)
...
@@ -48,8 +45,8 @@ module.exports = {
...
@@ -48,8 +45,8 @@ module.exports = {
}
}
// -> Strip host from local links
// -> Strip host from local links
if
(
isHostSet
&&
href
.
indexOf
(
`
${
WIKI
.
config
.
host
}
/`
)
===
0
)
{
if
(
hasHostname
&&
href
.
indexOf
(
`
${
this
.
site
.
hostname
}
/`
)
===
0
)
{
href
=
href
.
replace
(
WIKI
.
config
.
host
,
''
)
href
=
href
.
replace
(
this
.
site
.
hostname
,
''
)
}
}
// -> Assign local / external tag
// -> Assign local / external tag
...
@@ -68,7 +65,7 @@ module.exports = {
...
@@ -68,7 +65,7 @@ module.exports = {
let
pagePath
=
null
let
pagePath
=
null
// -> Add locale prefix if using namespacing
// -> Add locale prefix if using namespacing
if
(
WIKI
.
config
.
lang
.
n
amespacing
)
{
if
(
this
.
site
.
config
.
localeN
amespacing
)
{
// -> Reformat paths
// -> Reformat paths
if
(
href
.
indexOf
(
'/'
)
!==
0
)
{
if
(
href
.
indexOf
(
'/'
)
!==
0
)
{
if
(
this
.
config
.
absoluteLinks
)
{
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)
...
@@ -4,5 +4,5 @@ description: HTML Processing for diagrams (draw.io)
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-chart-multiline
icon
:
mdi-chart-multiline
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/html-image-prefetch/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Prefetch remotely rendered images (korki/plantuml)
...
@@ -4,5 +4,5 @@ description: Prefetch remotely rendered images (korki/plantuml)
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-cloud-download-outline
icon
:
mdi-cloud-download-outline
enabledDefault
:
false
enabledDefault
:
false
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/html-mediaplayers/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Embed players such as Youtube, Vimeo, Soundcloud, etc.
...
@@ -4,5 +4,5 @@ description: Embed players such as Youtube, Vimeo, Soundcloud, etc.
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-video
icon
:
mdi-video
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/html-mermaid/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Generate flowcharts from Mermaid syntax
...
@@ -4,5 +4,5 @@ description: Generate flowcharts from Mermaid syntax
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-arrow-decision-outline
icon
:
mdi-arrow-decision-outline
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/html-security/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: Filter and strips potentially dangerous content
...
@@ -4,7 +4,7 @@ description: Filter and strips potentially dangerous content
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-fire
icon
:
mdi-fire
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
step
:
post
step
:
post
order
:
99999
order
:
99999
props
:
props
:
...
...
server/modules/rendering/html-tabset/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Transform headers into tabs
...
@@ -4,5 +4,5 @@ description: Transform headers into tabs
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-tab
icon
:
mdi-tab
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/html-twemoji/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: Apply Twitter Emojis to all Unicode emojis
...
@@ -4,7 +4,7 @@ description: Apply Twitter Emojis to all Unicode emojis
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-emoticon-happy-outline
icon
:
mdi-emoticon-happy-outline
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
html
C
ore
dependsOn
:
html
-c
ore
step
:
post
step
:
post
order
:
10
order
:
10
props
:
{}
props
:
{}
server/modules/rendering/markdown-abbr/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Parse abbreviations into abbr tags
...
@@ -4,5 +4,5 @@ description: Parse abbreviations into abbr tags
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-contain-start
icon
:
mdi-contain-start
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/markdown-core/definition.yml
View file @
7128b160
key
:
markdown
C
ore
key
:
markdown
-c
ore
title
:
Core
title
:
Core
description
:
Basic Markdown Parser
description
:
Basic Markdown Parser
author
:
requarks.io
author
:
requarks.io
...
...
server/modules/rendering/markdown-core/renderer.js
View file @
7128b160
...
@@ -44,7 +44,7 @@ module.exports = {
...
@@ -44,7 +44,7 @@ module.exports = {
})
})
for
(
let
child
of
this
.
children
)
{
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
)
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
...
@@ -4,5 +4,5 @@ description: Convert tags to emojis
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-sticker-emoji
icon
:
mdi-sticker-emoji
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/markdown-expandtabs/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: Replace tabs with spaces in code blocks
...
@@ -4,7 +4,7 @@ description: Replace tabs with spaces in code blocks
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-arrow-expand-horizontal
icon
:
mdi-arrow-expand-horizontal
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
props
:
tabWidth
:
tabWidth
:
type
:
Number
type
:
Number
...
...
server/modules/rendering/markdown-footnotes/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Parse footnotes references
...
@@ -4,5 +4,5 @@ description: Parse footnotes references
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-page-layout-footer
icon
:
mdi-page-layout-footer
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/markdown-imsize/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Adds dimensions attributes to images
...
@@ -4,5 +4,5 @@ description: Adds dimensions attributes to images
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-image-size-select-large
icon
:
mdi-image-size-select-large
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
props
:
{}
server/modules/rendering/markdown-katex/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: LaTeX Math + Chemical Expression Typesetting Renderer
...
@@ -4,7 +4,7 @@ description: LaTeX Math + Chemical Expression Typesetting Renderer
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-math-integral
icon
:
mdi-math-integral
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
props
:
useInline
:
useInline
:
type
:
Boolean
type
:
Boolean
...
...
server/modules/rendering/markdown-kroki/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: Kroki Diagrams Parser
...
@@ -4,7 +4,7 @@ description: Kroki Diagrams Parser
author
:
rlanyi (based on PlantUML renderer)
author
:
rlanyi (based on PlantUML renderer)
icon
:
mdi-sitemap
icon
:
mdi-sitemap
enabledDefault
:
false
enabledDefault
:
false
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
props
:
server
:
server
:
type
:
String
type
:
String
...
...
server/modules/rendering/markdown-mathjax/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: LaTeX Math + Chemical Expression Typesetting Renderer
...
@@ -4,7 +4,7 @@ description: LaTeX Math + Chemical Expression Typesetting Renderer
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-math-integral
icon
:
mdi-math-integral
enabledDefault
:
false
enabledDefault
:
false
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
props
:
useInline
:
useInline
:
type
:
Boolean
type
:
Boolean
...
...
server/modules/rendering/markdown-multi-table/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: Add MultiMarkdown table support
...
@@ -4,7 +4,7 @@ description: Add MultiMarkdown table support
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-table
icon
:
mdi-table
enabledDefault
:
false
enabledDefault
:
false
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
props
:
multilineEnabled
:
multilineEnabled
:
type
:
Boolean
type
:
Boolean
...
...
server/modules/rendering/markdown-plantuml/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: PlantUML Markdown Parser
...
@@ -4,7 +4,7 @@ description: PlantUML Markdown Parser
author
:
ethanmdavidson
author
:
ethanmdavidson
icon
:
mdi-sitemap
icon
:
mdi-sitemap
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
props
:
server
:
server
:
type
:
String
type
:
String
...
...
server/modules/rendering/markdown-supsub/definition.yml
View file @
7128b160
...
@@ -4,7 +4,7 @@ description: Parse subscript and superscript tags
...
@@ -4,7 +4,7 @@ description: Parse subscript and superscript tags
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-format-superscript
icon
:
mdi-format-superscript
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
props
:
subEnabled
:
subEnabled
:
type
:
Boolean
type
:
Boolean
...
...
server/modules/rendering/markdown-tasklists/definition.yml
View file @
7128b160
...
@@ -4,5 +4,5 @@ description: Parse task lists to checkboxes
...
@@ -4,5 +4,5 @@ description: Parse task lists to checkboxes
author
:
requarks.io
author
:
requarks.io
icon
:
mdi-format-list-checks
icon
:
mdi-format-list-checks
enabledDefault
:
true
enabledDefault
:
true
dependsOn
:
markdown
C
ore
dependsOn
:
markdown
-c
ore
props
:
{}
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
...
@@ -5,8 +5,6 @@ block head
style(type='text/css')!= injectCode.css
style(type='text/css')!= injectCode.css
if injectCode.head
if injectCode.head
!= injectCode.head
!= injectCode.head
if config.features.featurePageComments
!= comments.head
block body
block body
#root
#root
...
@@ -21,20 +19,9 @@ block body
...
@@ -21,20 +19,9 @@ block body
author-name=page.authorName
author-name=page.authorName
:author-id=page.authorId
:author-id=page.authorId
editor=page.editorKey
editor=page.editorKey
:is-published=page.isPublished.toString()
toc=Buffer.from(page.toc).toString('base64')
:page-id=page.id
: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')
template(slot='contents')
div!= page.render
div!= page.render
template(slot='comments')
div!= comments.main
if injectCode.body
if injectCode.body
!= injectCode.body
!= injectCode.body
if config.features.featurePageComments
!= comments.body
server/worker.js
View file @
7128b160
...
@@ -12,7 +12,23 @@ let WIKI = {
...
@@ -12,7 +12,23 @@ let WIKI = {
INSTANCE_ID
:
'worker'
,
INSTANCE_ID
:
'worker'
,
SERVERPATH
:
path
.
join
(
process
.
cwd
(),
'server'
),
SERVERPATH
:
path
.
join
(
process
.
cwd
(),
'server'
),
Error
:
require
(
'./helpers/error'
),
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
global
.
WIKI
=
WIKI
...
...
ux/src/components/PageTags.vue
View file @
7128b160
<
template
lang=
"pug"
>
<
template
lang=
"pug"
>
.q-gutter-xs
.q-gutter-xs
template(v-if='
tags &&
tags.length > 0')
template(v-if='
pageStore.tags && pageStore.
tags.length > 0')
q-chip(
q-chip(
square
square
color='secondary'
color='secondary'
text-color='white'
text-color='white'
dense
dense
clickable
clickable
:removable='edit'
:removable='
props.
edit'
@remove='removeTag(tag)'
@remove='removeTag(tag)'
v-for='tag of tags'
v-for='tag of
pageStore.
tags'
:key='`tag-` + tag'
:key='`tag-` + tag'
)
)
q-icon.q-mr-xs(name='las la-tag', size='14px')
q-icon.q-mr-xs(name='las la-tag', size='14px')
span.text-caption
{{
tag
}}
span.text-caption
{{
tag
}}
q-chip(
q-chip(
v-if='!
edit &&
tags.length > 1'
v-if='!
props.edit && pageStore.
tags.length > 1'
square
square
color='secondary'
color='secondary'
text-color='white'
text-color='white'
...
@@ -24,36 +24,51 @@
...
@@ -24,36 +24,51 @@
)
)
q-icon(name='las la-tags', size='14px')
q-icon(name='las la-tags', size='14px')
q-input.q-mt-md(
q-input.q-mt-md(
v-if='edit'
v-if='
props.
edit'
outlined
outlined
v-model='newTag'
v-model='
state.
newTag'
dense
dense
placeholder='Add new tag...'
placeholder='Add new tag...'
)
)
</
template
>
</
template
>
<
script
>
<
script
setup
>
import
{
sync
}
from
'vuex-pathify'
import
{
useQuasar
}
from
'quasar'
import
{
reactive
}
from
'vue'
export
default
{
import
{
useI18n
}
from
'vue-i18n'
props
:
{
edit
:
{
import
{
usePageStore
}
from
'src/stores/page'
type
:
Boolean
,
default
:
false
// PROPS
}
},
const
props
=
defineProps
({
data
()
{
edit
:
{
return
{
type
:
Boolean
,
newTag
:
''
default
:
false
}
},
computed
:
{
tags
:
sync
(
'page/tags'
,
false
)
},
methods
:
{
removeTag
(
tag
)
{
this
.
tags
=
this
.
tags
.
filter
(
t
=>
t
!==
tag
)
}
}
}
})
// 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
>
</
script
>
ux/src/components/SocialSharingMenu.vue
View file @
7128b160
...
@@ -11,125 +11,146 @@ q-menu(
...
@@ -11,125 +11,146 @@ q-menu(
q-item-section.items-center(avatar)
q-item-section.items-center(avatar)
q-icon(color='grey', name='las la-clipboard', size='sm')
q-icon(color='grey', name='las la-clipboard', size='sm')
q-item-section.q-pr-md Copy URL
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-item-section.items-center(avatar)
q-icon(color='grey', name='las la-envelope', size='sm')
q-icon(color='grey', name='las la-envelope', size='sm')
q-item-section.q-pr-md Email
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-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-facebook', size='sm')
q-icon(color='grey', name='lab la-facebook', size='sm')
q-item-section.q-pr-md Facebook
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-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-linkedin', size='sm')
q-icon(color='grey', name='lab la-linkedin', size='sm')
q-item-section.q-pr-md LinkedIn
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-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-reddit', size='sm')
q-icon(color='grey', name='lab la-reddit', size='sm')
q-item-section.q-pr-md Reddit
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-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-telegram', size='sm')
q-icon(color='grey', name='lab la-telegram', size='sm')
q-item-section.q-pr-md Telegram
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-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-twitter', size='sm')
q-icon(color='grey', name='lab la-twitter', size='sm')
q-item-section.q-pr-md Twitter
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-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-viber', size='sm')
q-icon(color='grey', name='lab la-viber', size='sm')
q-item-section.q-pr-md Viber
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-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-weibo', size='sm')
q-icon(color='grey', name='lab la-weibo', size='sm')
q-item-section.q-pr-md Weibo
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-item-section.items-center(avatar)
q-icon(color='grey', name='lab la-whatsapp', size='sm')
q-icon(color='grey', name='lab la-whatsapp', size='sm')
q-item-section.q-pr-md Whatsapp
q-item-section.q-pr-md Whatsapp
</
template
>
</
template
>
<
script
>
<
script
setup
>
import
ClipboardJS
from
'clipboard'
import
ClipboardJS
from
'clipboard'
import
{
useQuasar
}
from
'quasar'
import
{
computed
,
onMounted
,
reactive
,
ref
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
export
default
{
// PROPS
props
:
{
url
:
{
const
props
=
defineProps
({
type
:
String
,
url
:
{
default
:
null
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
''
}
}
},
},
methods
:
{
title
:
{
openSocialPop
(
url
)
{
type
:
String
,
const
popupWindow
=
window
.
open
(
default
:
'Untitled Page'
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
()
{
description
:
{
/**
type
:
String
,
* Center the popup on dual screens
default
:
''
* 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
// QUASAR
const
width
=
window
.
innerWidth
?
window
.
innerWidth
:
(
document
.
documentElement
.
clientWidth
?
document
.
documentElement
.
clientWidth
:
screen
.
width
)
const
$q
=
useQuasar
()
const
height
=
window
.
innerHeight
?
window
.
innerHeight
:
(
document
.
documentElement
.
clientHeight
?
document
.
documentElement
.
clientHeight
:
screen
.
height
)
// I18N
this
.
left
=
((
width
/
2
)
-
(
this
.
width
/
2
))
+
dualScreenLeft
this
.
top
=
((
height
/
2
)
-
(
this
.
height
/
2
))
+
dualScreenTop
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
>
</
script
>
ux/src/i18n/locales/en.json
View file @
7128b160
...
@@ -1540,5 +1540,9 @@
...
@@ -1540,5 +1540,9 @@
"admin.instances.lastSeen"
:
"Last Seen"
,
"admin.instances.lastSeen"
:
"Last Seen"
,
"admin.instances.firstSeen"
:
"First Seen"
,
"admin.instances.firstSeen"
:
"First Seen"
,
"admin.instances.activeListeners"
:
"Active Listeners"
,
"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')
...
@@ -3,7 +3,7 @@ q-layout.admin(view='hHh Lpr lff')
q-header.bg-black.text-white
q-header.bg-black.text-white
.row.no-wrap
.row.no-wrap
q-toolbar(style='height: 64px;', dark)
q-toolbar(style='height: 64px;', dark)
q-btn(dense, flat,
href
='/')
q-btn(dense, flat,
to
='/')
q-avatar(size='34px', square)
q-avatar(size='34px', square)
img(src='/_assets/logo-wikijs.svg')
img(src='/_assets/logo-wikijs.svg')
q-toolbar-title.text-h6 Wiki.js
q-toolbar-title.text-h6 Wiki.js
...
@@ -102,10 +102,6 @@ q-layout.admin(view='hHh Lpr lff')
...
@@ -102,10 +102,6 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-tree-structure.svg')
q-icon(name='img:/_assets/icons/fluent-tree-structure.svg')
q-item-section
{{
t
(
'admin.navigation.title'
)
}}
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(:to='`/_admin/` + adminStore.currentSiteId + `/storage`', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-ssd.svg')
q-icon(name='img:/_assets/icons/fluent-ssd.svg')
...
@@ -156,6 +152,10 @@ q-layout.admin(view='hHh Lpr lff')
...
@@ -156,6 +152,10 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section
{{
t
(
'admin.mail.title'
)
}}
q-item-section
{{
t
(
'admin.mail.title'
)
}}
q-item-section(side)
q-item-section(side)
status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`')
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(to='/_admin/scheduler', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-bot.svg')
q-icon(name='img:/_assets/icons/fluent-bot.svg')
...
...
ux/src/layouts/MainLayout.vue
View file @
7128b160
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
q-layout(view='hHh Lpr lff')
q-layout(view='hHh Lpr lff')
header-nav
header-nav
q-drawer.bg-sidebar(
q-drawer.bg-sidebar(
v-model='showSideNav'
v-model='s
iteStore.s
howSideNav'
show-if-above
show-if-above
:width='255'
:width='255'
)
)
...
@@ -81,46 +81,64 @@ q-layout(view='hHh Lpr lff')
...
@@ -81,46 +81,64 @@ q-layout(view='hHh Lpr lff')
span(style='font-size: 11px;') © Cyberdyne Systems Corp. 2020 | Powered by #[strong Wiki.js]
span(style='font-size: 11px;') © Cyberdyne Systems Corp. 2020 | Powered by #[strong Wiki.js]
</
template
>
</
template
>
<
script
>
<
script
setup
>
import
{
get
,
sync
}
from
'vuex-pathify'
import
{
useMeta
,
useQuasar
,
setCssVar
}
from
'quasar'
import
{
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'
import
HeaderNav
from
'../components/HeaderNav.vue'
export
default
{
// QUASAR
name
:
'MainLayout'
,
components
:
{
const
$q
=
useQuasar
()
HeaderNav
},
// STORES
data
()
{
return
{
const
siteStore
=
useSiteStore
()
leftDrawerOpen
:
true
,
search
:
''
,
// ROUTER
thumbStyle
:
{
right
:
'2px'
,
const
router
=
useRouter
()
borderRadius
:
'5px'
,
const
route
=
useRoute
()
backgroundColor
:
'#FFF'
,
width
:
'5px'
,
// I18N
opacity
:
0.5
},
const
{
t
}
=
useI18n
()
barStyle
:
{
backgroundColor
:
'#000'
,
// META
width
:
'9px'
,
opacity
:
0.1
useMeta
({
}
titleTemplate
:
title
=>
`
${
title
}
-
${
siteStore
.
title
}
`
}
})
},
computed
:
{
// DATA
showSideNav
:
sync
(
'site/showSideNav'
,
false
),
isSyncing
:
get
(
'isLoading'
,
false
)
const
leftDrawerOpen
=
ref
(
true
)
},
const
search
=
ref
(
''
)
created
()
{
const
user
=
reactive
({
setCssVar
(
'primary'
,
this
.
$store
.
get
(
'site/theme@colorPrimary'
))
name
:
'John Doe'
,
setCssVar
(
'secondary'
,
this
.
$store
.
get
(
'site/theme@colorSecondary'
))
email
:
'test@example.com'
,
setCssVar
(
'accent'
,
this
.
$store
.
get
(
'site/theme@colorAccent'
))
picture
:
null
setCssVar
(
'header'
,
this
.
$store
.
get
(
'site/theme@colorHeader'
))
})
setCssVar
(
'sidebar'
,
this
.
$store
.
get
(
'site/theme@colorSidebar'
))
const
thumbStyle
=
{
}
right
:
'2px'
,
borderRadius
:
'5px'
,
backgroundColor
:
'#FFF'
,
width
:
'5px'
,
opacity
:
0.5
}
}
const
barStyle
=
{
backgroundColor
:
'#000'
,
width
:
'9px'
,
opacity
:
0.1
}
</
script
>
</
script
>
<
style
lang=
"scss"
>
<
style
lang=
"scss"
>
...
...
ux/src/pages/AdminScheduler.vue
View file @
7128b160
...
@@ -132,7 +132,7 @@ q-page.admin-terminal
...
@@ -132,7 +132,7 @@ q-page.admin-terminal
div: small.text-grey
{{
humanizeDate
(
props
.
row
.
waitUntil
)
}}
div: small.text-grey
{{
humanizeDate
(
props
.
row
.
waitUntil
)
}}
template(v-slot:body-cell-retries='props')
template(v-slot:body-cell-retries='props')
q-td(:props='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')
template(v-slot:body-cell-useworker='props')
q-td(:props='props')
q-td(:props='props')
template(v-if='props.value')
template(v-if='props.value')
...
@@ -148,6 +148,15 @@ q-page.admin-terminal
...
@@ -148,6 +148,15 @@ q-page.admin-terminal
i18n-t.text-grey(keypath='admin.scheduler.createdBy', tag='small')
i18n-t.text-grey(keypath='admin.scheduler.createdBy', tag='small')
template(#instance)
template(#instance)
strong
{{
props
.
row
.
createdBy
}}
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)
template(v-else)
q-card.rounded-borders(
q-card.rounded-borders(
v-if='state.jobs.length < 1'
v-if='state.jobs.length < 1'
...
@@ -221,7 +230,7 @@ q-page.admin-terminal
...
@@ -221,7 +230,7 @@ q-page.admin-terminal
div: small
{{
props
.
row
.
lastErrorMessage
}}
div: small
{{
props
.
row
.
lastErrorMessage
}}
template(v-slot:body-cell-attempt='props')
template(v-slot:body-cell-attempt='props')
q-td(:props='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')
template(v-slot:body-cell-useworker='props')
q-td(:props='props')
q-td(:props='props')
template(v-if='props.value')
template(v-if='props.value')
...
@@ -238,6 +247,17 @@ q-page.admin-terminal
...
@@ -238,6 +247,17 @@ q-page.admin-terminal
i18n-t.text-grey(keypath='admin.scheduler.createdBy', tag='small')
i18n-t.text-grey(keypath='admin.scheduler.createdBy', tag='small')
template(#instance)
template(#instance)
strong
{{
props
.
row
.
executedBy
}}
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
>
</
template
>
...
@@ -271,7 +291,7 @@ useMeta({
...
@@ -271,7 +291,7 @@ useMeta({
// DATA
// DATA
const
state
=
reactive
({
const
state
=
reactive
({
displayMode
:
'
completed
'
,
displayMode
:
'
upcoming
'
,
scheduledJobs
:
[],
scheduledJobs
:
[],
upcomingJobs
:
[],
upcomingJobs
:
[],
jobs
:
[],
jobs
:
[],
...
@@ -369,6 +389,13 @@ const upcomingJobsHeaders = [
...
@@ -369,6 +389,13 @@ const upcomingJobsHeaders = [
name
:
'date'
,
name
:
'date'
,
sortable
:
true
,
sortable
:
true
,
format
:
v
=>
DateTime
.
fromISO
(
v
).
toRelative
()
format
:
v
=>
DateTime
.
fromISO
(
v
).
toRelative
()
},
{
align
:
'center'
,
field
:
'id'
,
name
:
'cancel'
,
sortable
:
false
,
style
:
'width: 15px;'
}
}
]
]
...
@@ -415,6 +442,13 @@ const jobsHeaders = [
...
@@ -415,6 +442,13 @@ const jobsHeaders = [
name
:
'date'
,
name
:
'date'
,
sortable
:
true
,
sortable
:
true
,
format
:
v
=>
DateTime
.
fromISO
(
v
).
toRelative
()
format
:
v
=>
DateTime
.
fromISO
(
v
).
toRelative
()
},
{
align
:
'center'
,
field
:
'id'
,
name
:
'actions'
,
sortable
:
false
,
style
:
'width: 15px;'
}
}
]
]
...
@@ -524,6 +558,80 @@ async function load () {
...
@@ -524,6 +558,80 @@ async function load () {
state
.
loading
--
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
// MOUNTED
onMounted
(()
=>
{
onMounted
(()
=>
{
...
...
ux/src/pages/Index.vue
View file @
7128b160
...
@@ -13,97 +13,33 @@ q-page.column
...
@@ -13,97 +13,33 @@ q-page.column
q-breadcrumbs-el(icon='las la-home', to='/', aria-label='Home')
q-breadcrumbs-el(icon='las la-home', to='/', aria-label='Home')
q-tooltip Home
q-tooltip Home
q-breadcrumbs-el(
q-breadcrumbs-el(
v-for='brd of breadcrumbs'
v-for='brd of
pageStore.
breadcrumbs'
:key='brd.id'
:key='brd.id'
:icon='brd.icon'
:icon='brd.icon'
:label='brd.title'
:label='brd.title'
:aria-label='brd.title'
:aria-label='brd.title'
:to='$pageHelpers.getFullPath(brd)'
:to='getFullPath(brd)'
)
q-breadcrumbs-el(
v-if='editCreateMode'
:icon='pageIcon'
:label='title || `Untitled Page`'
:aria-label='title || `Untitled Page`'
)
)
.col-auto.flex.items-center.justify-end
.col-auto.flex.items-center.justify-end
template(v-if='!isPublished')
template(v-if='!
pageStore.
isPublished')
.text-caption.text-accent: strong Unpublished
.text-caption.text-accent: strong Unpublished
q-separator.q-mx-sm(vertical)
q-separator.q-mx-sm(vertical)
.text-caption.text-grey-6(v-if='editCreateMode') New Page
.text-caption.text-grey-6 Last modified on #[strong September 5th, 2020]
.text-caption.text-grey-6(v-if='!editCreateMode') Last modified on #[strong September 5th, 2020]
.page-header.row
.page-header.row
//- PAGE ICON
//- PAGE ICON
.col-auto.q-pl-md.flex.items-center(v-if='editMode')
.col-auto.q-pl-md.flex.items-center
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)
q-icon.rounded-borders(
q-icon.rounded-borders(
:name='page
I
con'
:name='page
Store.i
con'
size='64px'
size='64px'
color='primary'
color='primary'
)
)
//- PAGE HEADER
//- PAGE HEADER
.col.q-pa-md(v-if='editMode')
.col.q-pa-md
q-input.no-height(
.text-h4.page-header-title
{{
pageStore
.
title
}}
borderless
.text-subtitle2.page-header-subtitle
{{
pageStore
.
description
}}
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
}}
//- PAGE ACTIONS
//- PAGE ACTIONS
.col-auto.q-pa-md.flex.items-center.justify-end(v-if='editMode')
.col-auto.q-pa-md.flex.items-center.justify-end
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)
q-btn.q-mr-md(
q-btn.q-mr-md(
flat
flat
dense
dense
...
@@ -144,23 +80,18 @@ q-page.column
...
@@ -144,23 +80,18 @@ q-page.column
label='Edit'
label='Edit'
aria-label='Edit'
aria-label='Edit'
no-caps
no-caps
@click='mode = `edit`
'
:href='editUrl
'
)
)
.page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
.page-container.row.no-wrap.items-stretch(style='flex: 1 1 100%;')
.col(style='order: 1;')
.col(style='order: 1;')
q-no-ssr(v-if='editMode')
component(:is='editorComponent')
//- editor-wysiwyg
//- editor-markdown
q-scroll-area(
q-scroll-area(
:thumb-style='thumbStyle'
:thumb-style='thumbStyle'
:bar-style='barStyle'
:bar-style='barStyle'
style='height: 100%;'
style='height: 100%;'
v-else
)
)
.q-pa-md
.q-pa-md
div(v-html='render')
div(v-html='
pageStore.
render')
template(v-if='
relations &&
relations.length > 0')
template(v-if='
pageStore.relations && pageStore.
relations.length > 0')
q-separator.q-my-lg
q-separator.q-my-lg
.row.align-center
.row.align-center
.col.text-left(v-if='relationsLeft.length > 0')
.col.text-left(v-if='relationsLeft.length > 0')
...
@@ -204,24 +135,24 @@ q-page.column
...
@@ -204,24 +135,24 @@ q-page.column
v-if='showSidebar'
v-if='showSidebar'
style='order: 2;'
style='order: 2;'
)
)
template(v-if='showToc')
template(v-if='
pageStore.
showToc')
//- TOC
//- TOC
.q-pa-md.flex.items-center
.q-pa-md.flex.items-center
q-icon.q-mr-sm(name='las la-stream', color='grey')
q-icon.q-mr-sm(name='las la-stream', color='grey')
.text-caption.text-grey-7 Contents
.text-caption.text-grey-7 Contents
.q-px-md.q-pb-sm
.q-px-md.q-pb-sm
q-tree(
q-tree(
:nodes='toc'
:nodes='
state.
toc'
node-key='key'
node-key='key'
v-model:expanded='tocExpanded'
v-model:expanded='
state.
tocExpanded'
v-model:selected='tocSelected'
v-model:selected='
state.
tocSelected'
)
)
//- Tags
//- Tags
template(v-if='showTags')
template(v-if='
pageStore.
showTags')
q-separator(v-if='showToc')
q-separator(v-if='
pageStore.
showToc')
.q-pa-md(
.q-pa-md(
@mouseover='showTagsEditBtn = true'
@mouseover='s
tate.s
howTagsEditBtn = true'
@mouseleave='showTagsEditBtn = false'
@mouseleave='s
tate.s
howTagsEditBtn = false'
)
)
.flex.items-center
.flex.items-center
q-icon.q-mr-sm(name='las la-tags', color='grey')
q-icon.q-mr-sm(name='las la-tags', color='grey')
...
@@ -229,7 +160,7 @@ q-page.column
...
@@ -229,7 +160,7 @@ q-page.column
q-space
q-space
transition(name='fade')
transition(name='fade')
q-btn(
q-btn(
v-show='showTagsEditBtn'
v-show='s
tate.s
howTagsEditBtn'
size='sm'
size='sm'
padding='none xs'
padding='none xs'
icon='las la-pen'
icon='las la-pen'
...
@@ -237,24 +168,24 @@ q-page.column
...
@@ -237,24 +168,24 @@ q-page.column
flat
flat
label='Edit'
label='Edit'
no-caps
no-caps
@click='
tagEditMode = !
tagEditMode'
@click='
state.tagEditMode = !state.
tagEditMode'
)
)
page-tags.q-mt-sm(:edit='tagEditMode')
page-tags.q-mt-sm(:edit='
state.
tagEditMode')
template(v-if='
allowRatings &&
ratingsMode !== `off`')
template(v-if='
pageStore.allowRatings && pageStore.
ratingsMode !== `off`')
q-separator(v-if='
showToc ||
showTags')
q-separator(v-if='
pageStore.showToc || pageStore.
showTags')
//- Rating
//- Rating
.q-pa-md.flex.items-center
.q-pa-md.flex.items-center
q-icon.q-mr-sm(name='las la-star-half-alt', color='grey')
q-icon.q-mr-sm(name='las la-star-half-alt', color='grey')
.text-caption.text-grey-7 Rate this page
.text-caption.text-grey-7 Rate this page
.q-px-md
.q-px-md
q-rating(
q-rating(
v-if='ratingsMode === `stars`'
v-if='
pageStore.
ratingsMode === `stars`'
v-model='currentRating'
v-model='
state.
currentRating'
icon='las la-star'
icon='las la-star'
color='secondary'
color='secondary'
size='sm'
size='sm'
)
)
.flex.items-center(v-else-if='ratingsMode === `thumbs`')
.flex.items-center(v-else-if='
pageStore.
ratingsMode === `thumbs`')
q-btn.acrylic-btn(
q-btn.acrylic-btn(
flat
flat
icon='las la-thumbs-down'
icon='las la-thumbs-down'
...
@@ -350,206 +281,226 @@ q-page.column
...
@@ -350,206 +281,226 @@ q-page.column
q-tooltip(anchor='center left' self='center right') Delete Page
q-tooltip(anchor='center left' self='center right') Delete Page
q-dialog(
q-dialog(
v-model='showSideDialog'
v-model='s
tate.s
howSideDialog'
position='right'
position='right'
full-height
full-height
transition-show='jump-left'
transition-show='jump-left'
transition-hide='jump-right'
transition-hide='jump-right'
class='floating-sidepanel'
class='floating-sidepanel'
)
)
component(:is='sideDialogComponent')
component(:is='s
tate.s
ideDialogComponent')
q-dialog(
q-dialog(
v-model='showGlobalDialog'
v-model='s
tate.s
howGlobalDialog'
transition-show='jump-up'
transition-show='jump-up'
transition-hide='jump-down'
transition-hide='jump-down'
)
)
component(:is='globalDialogComponent')
component(:is='
state.
globalDialogComponent')
</
template
>
</
template
>
<
script
>
<
script
setup
>
import
{
get
,
sync
}
from
'vuex-pathify'
import
{
useMeta
,
useQuasar
,
setCssVar
}
from
'quasar'
import
IconPickerDialog
from
'../components/IconPickerDialog.vue'
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
SocialSharingMenu
from
'../components/SocialSharingMenu.vue'
import
PageDataDialog
from
'../components/PageDataDialog.vue'
import
PageDataDialog
from
'../components/PageDataDialog.vue'
import
PageTags
from
'../components/PageTags.vue'
import
PageTags
from
'../components/PageTags.vue'
import
PagePropertiesDialog
from
'../components/PagePropertiesDialog.vue'
import
PagePropertiesDialog
from
'../components/PagePropertiesDialog.vue'
import
PageSaveDialog
from
'../components/PageSaveDialog.vue'
import
PageSaveDialog
from
'../components/PageSaveDialog.vue'
import
EditorWysiwyg
from
'../components/EditorWysiwyg.vue'
// QUASAR
export
default
{
name
:
'PageIndex'
,
const
$q
=
useQuasar
()
components
:
{
EditorWysiwyg
,
// STORES
IconPickerDialog
,
PageDataDialog
,
const
pageStore
=
usePageStore
()
PagePropertiesDialog
,
const
siteStore
=
useSiteStore
()
PageSaveDialog
,
PageTags
,
// ROUTER
SocialSharingMenu
},
const
router
=
useRouter
()
data
()
{
const
route
=
useRoute
()
return
{
showSideDialog
:
false
,
// I18N
sideDialogComponent
:
null
,
showGlobalDialog
:
false
,
const
{
t
}
=
useI18n
()
globalDialogComponent
:
null
,
showTagsEditBtn
:
false
,
// META
tagEditMode
:
false
,
toc
:
[
useMeta
({
{
title
:
pageStore
.
title
key
:
'h1-0'
,
})
label
:
'Introduction'
},
// 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
'
,
key
:
'h
2-0
'
,
label
:
'
Planets
'
,
label
:
'
Earth
'
,
children
:
[
children
:
[
{
{
key
:
'h
2
-0'
,
key
:
'h
3
-0'
,
label
:
'
Earth
'
,
label
:
'
Countries
'
,
children
:
[
children
:
[
{
{
key
:
'h
3
-0'
,
key
:
'h
4
-0'
,
label
:
'C
ountr
ies'
,
label
:
'C
it
ies'
,
children
:
[
children
:
[
{
{
key
:
'h
4
-0'
,
key
:
'h
5
-0'
,
label
:
'
Cities
'
,
label
:
'
Montreal
'
,
children
:
[
children
:
[
{
{
key
:
'h5-0'
,
key
:
'h6-0'
,
label
:
'Montreal'
,
label
:
'Districts'
children
:
[
{
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
:
{
tocExpanded
:
[
'h1-0'
,
'h1-1'
],
toc
()
{
tocSelected
:
[],
this
.
refreshTocExpanded
()
currentRating
:
3
},
})
tocDepth
()
{
const
thumbStyle
=
{
this
.
refreshTocExpanded
()
right
:
'2px'
,
}
borderRadius
:
'5px'
,
},
backgroundColor
:
'#000'
,
mounted
()
{
width
:
'5px'
,
this
.
refreshTocExpanded
()
opacity
:
0.15
},
}
methods
:
{
const
barStyle
=
{
togglePageProperties
()
{
backgroundColor
:
'#FAFAFA'
,
this
.
sideDialogComponent
=
'PagePropertiesDialog'
width
:
'9px'
,
this
.
showSideDialog
=
true
opacity
:
1
},
}
togglePageData
()
{
this
.
sideDialogComponent
=
'PageDataDialog'
// COMPUTED
this
.
showSideDialog
=
true
},
const
showSidebar
=
computed
(()
=>
{
savePage
()
{
return
pageStore
.
showSidebar
&&
siteStore
.
showSidebar
this
.
globalDialogComponent
=
'PageSaveDialog'
})
this
.
showGlobalDialog
=
true
const
editorComponent
=
computed
(()
=>
{
},
return
pageStore
.
editor
?
`editor-
${
pageStore
.
editor
}
`
:
null
refreshTocExpanded
(
baseToc
)
{
})
const
toExpand
=
[]
const
relationsLeft
=
computed
(()
=>
{
let
isRootNode
=
false
return
pageStore
.
relations
?
pageStore
.
relations
.
filter
(
r
=>
r
.
position
===
'left'
)
:
[]
if
(
!
baseToc
)
{
})
baseToc
=
this
.
toc
const
relationsCenter
=
computed
(()
=>
{
isRootNode
=
true
return
pageStore
.
relations
?
pageStore
.
relations
.
filter
(
r
=>
r
.
position
===
'center'
)
:
[]
}
})
if
(
baseToc
.
length
>
0
)
{
const
relationsRight
=
computed
(()
=>
{
for
(
const
node
of
baseToc
)
{
return
pageStore
.
relations
?
pageStore
.
relations
.
filter
(
r
=>
r
.
position
===
'right'
)
:
[]
if
(
node
.
key
>=
`h
${
this
.
tocDepth
.
min
}
`
&&
node
.
key
<=
`h
${
this
.
tocDepth
.
max
}
`
)
{
})
toExpand
.
push
(
node
.
key
)
const
editMode
=
computed
(()
=>
{
}
return
pageStore
.
mode
===
'edit'
if
(
node
.
children
?.
length
&&
node
.
key
<
`h
${
this
.
tocDepth
.
max
}
`
)
{
})
toExpand
.
push
(...
this
.
refreshTocExpanded
(
node
.
children
))
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
)
{
if
(
node
.
children
?.
length
&&
node
.
key
<
`h
${
pageStore
.
tocDepth
.
max
}
`
)
{
this
.
tocExpanded
=
toExpand
toExpand
.
push
(...
refreshTocExpanded
(
node
.
children
))
}
else
{
return
toExpand
}
}
}
}
}
}
if
(
isRootNode
)
{
state
.
tocExpanded
=
toExpand
}
else
{
return
toExpand
}
}
}
// MOUNTED
onMounted
(()
=>
{
refreshTocExpanded
()
})
</
script
>
</
script
>
<
style
lang=
"scss"
>
<
style
lang=
"scss"
>
...
...
ux/src/router/routes.js
View file @
7128b160
const
routes
=
[
const
routes
=
[
//
{
{
//
path: '/',
path
:
'/'
,
//
component: () => import('../layouts/MainLayout.vue'),
component
:
()
=>
import
(
'../layouts/MainLayout.vue'
),
//
children: [
children
:
[
// { path: '', component: () => import('../pages/Index.vue') },
{
path
:
''
,
component
:
()
=>
import
(
'../pages/Index.vue'
)
}
//
{ path: 'n/:editor?', component: () => import('../pages/Index.vue') }
//
{ path: 'n/:editor?', component: () => import('../pages/Index.vue') }
//
]
]
//
},
},
{
{
path
:
'/login'
,
path
:
'/login'
,
component
:
()
=>
import
(
'layouts/AuthLayout.vue'
),
component
:
()
=>
import
(
'layouts/AuthLayout.vue'
),
...
@@ -36,7 +36,6 @@ const routes = [
...
@@ -36,7 +36,6 @@ const routes = [
{
path
:
':siteid/locale'
,
component
:
()
=>
import
(
'pages/AdminLocale.vue'
)
},
{
path
:
':siteid/locale'
,
component
:
()
=>
import
(
'pages/AdminLocale.vue'
)
},
{
path
:
':siteid/login'
,
component
:
()
=>
import
(
'pages/AdminLogin.vue'
)
},
{
path
:
':siteid/login'
,
component
:
()
=>
import
(
'pages/AdminLogin.vue'
)
},
{
path
:
':siteid/navigation'
,
component
:
()
=>
import
(
'pages/AdminNavigation.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/storage/:id?'
,
component
:
()
=>
import
(
'pages/AdminStorage.vue'
)
},
{
path
:
':siteid/theme'
,
component
:
()
=>
import
(
'pages/AdminTheme.vue'
)
},
{
path
:
':siteid/theme'
,
component
:
()
=>
import
(
'pages/AdminTheme.vue'
)
},
// -> Users
// -> Users
...
@@ -48,6 +47,7 @@ const routes = [
...
@@ -48,6 +47,7 @@ const routes = [
{
path
:
'extensions'
,
component
:
()
=>
import
(
'pages/AdminExtensions.vue'
)
},
{
path
:
'extensions'
,
component
:
()
=>
import
(
'pages/AdminExtensions.vue'
)
},
{
path
:
'instances'
,
component
:
()
=>
import
(
'pages/AdminInstances.vue'
)
},
{
path
:
'instances'
,
component
:
()
=>
import
(
'pages/AdminInstances.vue'
)
},
{
path
:
'mail'
,
component
:
()
=>
import
(
'pages/AdminMail.vue'
)
},
{
path
:
'mail'
,
component
:
()
=>
import
(
'pages/AdminMail.vue'
)
},
// { path: 'rendering', component: () => import('pages/AdminRendering.vue') },
{
path
:
'scheduler'
,
component
:
()
=>
import
(
'pages/AdminScheduler.vue'
)
},
{
path
:
'scheduler'
,
component
:
()
=>
import
(
'pages/AdminScheduler.vue'
)
},
{
path
:
'security'
,
component
:
()
=>
import
(
'pages/AdminSecurity.vue'
)
},
{
path
:
'security'
,
component
:
()
=>
import
(
'pages/AdminSecurity.vue'
)
},
{
path
:
'system'
,
component
:
()
=>
import
(
'pages/AdminSystem.vue'
)
},
{
path
:
'system'
,
component
:
()
=>
import
(
'pages/AdminSystem.vue'
)
},
...
@@ -74,7 +74,10 @@ const routes = [
...
@@ -74,7 +74,10 @@ const routes = [
// but you can also remove it
// but you can also remove it
{
{
path
:
'/:catchAll(.*)*'
,
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