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
4cdeba80
Unverified
Commit
4cdeba80
authored
Dec 18, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: page browser dialog + various improvements
parent
2a051637
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
782 additions
and
193 deletions
+782
-193
nav-header.vue
client/components/common/nav-header.vue
+0
-6
editor-modal-editorselect.vue
client/components/editor/editor-modal-editorselect.vue
+0
-20
3.0.0.js
server/db/migrations/3.0.0.js
+2
-10
asset.js
server/graph/resolvers/asset.js
+48
-2
page.graphql
server/graph/schemas/page.graphql
+2
-2
site.graphql
server/graph/schemas/site.graphql
+11
-3
base.pug
server/views/base.pug
+10
-2
fluent-save-as.svg
ux/public/_assets/icons/fluent-save-as.svg
+2
-0
ultraviolet-confusion.svg
ux/public/_assets/icons/ultraviolet-confusion.svg
+2
-0
ultraviolet-new-document.svg
ux/public/_assets/icons/ultraviolet-new-document.svg
+2
-0
FileManager.vue
ux/src/components/FileManager.vue
+32
-3
PageBrowser.vue
ux/src/components/PageBrowser.vue
+0
-42
PageDeleteDialog.vue
ux/src/components/PageDeleteDialog.vue
+109
-0
PageSaveDialog.vue
ux/src/components/PageSaveDialog.vue
+362
-43
TreeLevel.vue
ux/src/components/TreeLevel.vue
+15
-3
TreeNav.vue
ux/src/components/TreeNav.vue
+52
-0
TreeNode.vue
ux/src/components/TreeNode.vue
+12
-20
en.json
ux/src/i18n/locales/en.json
+18
-1
AdminGeneral.vue
ux/src/pages/AdminGeneral.vue
+25
-3
AdminUtilities.vue
ux/src/pages/AdminUtilities.vue
+13
-1
Index.vue
ux/src/pages/Index.vue
+55
-31
site.js
ux/src/stores/site.js
+10
-1
No files found.
client/components/common/nav-header.vue
View file @
4cdeba80
...
...
@@ -241,12 +241,6 @@
page-selector(mode='create', v-model='duplicateOpts.modal', :open-handler='pageDuplicateHandle', :path='duplicateOpts.path', :locale='duplicateOpts.locale')
page-delete(v-model='deletePageModal', v-if='path && path.length')
page-convert(v-model='convertPageModal', v-if='path && path.length')
.nav-header-dev(v-if='isDevMode')
v-icon mdi-alert
div
.overline DEVELOPMENT VERSION
.overline This code base is NOT for production use!
</
template
>
<
script
>
...
...
client/components/editor/editor-modal-editorselect.vue
View file @
4cdeba80
...
...
@@ -36,26 +36,6 @@
img(src='/_assets-legacy/svg/editor-icon-ckeditor.svg', alt='Visual Editor', style='width: 36px;')
.body-2.mt-2.primary--text Visual Editor
.caption.grey--text Rich-text WYSIWYG
v-card.radius-7.mt-2(color='teal darken-3', dark)
v-card-text.text-center.py-4
.subtitle-1.white--text
{{
$t
(
'editor:select.customView'
)
}}
v-container(grid-list-lg, fluid)
v-layout(row, wrap, justify-center)
v-flex(xs4)
v-hover
template(v-slot:default='{ hover }')
v-card.radius-7.animated.fadeInUp(
hover
light
ripple
)
v-card-text.text-center(@click='fromTemplate')
img(src='/_assets-legacy/svg/icon-cube.svg', alt='From Template', style='width: 42px; opacity: .5;')
.body-2.mt-1.teal--text From Template
.caption.grey--text Use an existing page...
page-selector(mode='select', v-model='templateDialogIsShown', :open-handler='fromTemplateHandle', :path='path', :locale='locale', must-exist)
</
template
>
<
script
>
...
...
server/db/migrations/3.0.0.js
View file @
4cdeba80
...
...
@@ -58,12 +58,7 @@ exports.up = async knex => {
.
createTable
(
'assetData'
,
table
=>
{
table
.
uuid
(
'id'
).
notNullable
().
primary
()
table
.
binary
(
'data'
).
notNullable
()
})
// ASSET FOLDERS -----------------------
.
createTable
(
'assetFolders'
,
table
=>
{
table
.
uuid
(
'id'
).
notNullable
().
primary
().
defaultTo
(
knex
.
raw
(
'gen_random_uuid()'
))
table
.
string
(
'name'
).
notNullable
()
table
.
string
(
'slug'
).
notNullable
()
table
.
binary
(
'preview'
)
})
// AUTHENTICATION ----------------------
.
createTable
(
'authentication'
,
table
=>
{
...
...
@@ -351,13 +346,9 @@ exports.up = async knex => {
table
.
uuid
(
'siteId'
).
notNullable
().
references
(
'id'
).
inTable
(
'sites'
)
})
.
table
(
'assets'
,
table
=>
{
table
.
uuid
(
'folderId'
).
notNullable
().
references
(
'id'
).
inTable
(
'assetFolders'
).
index
()
table
.
uuid
(
'authorId'
).
notNullable
().
references
(
'id'
).
inTable
(
'users'
)
table
.
uuid
(
'siteId'
).
notNullable
().
references
(
'id'
).
inTable
(
'sites'
).
index
()
})
.
table
(
'assetFolders'
,
table
=>
{
table
.
uuid
(
'parentId'
).
references
(
'id'
).
inTable
(
'assetFolders'
).
index
()
})
.
table
(
'commentProviders'
,
table
=>
{
table
.
uuid
(
'siteId'
).
notNullable
().
references
(
'id'
).
inTable
(
'sites'
)
})
...
...
@@ -551,6 +542,7 @@ exports.up = async knex => {
comments
:
false
,
contributions
:
false
,
profile
:
true
,
reasonForChange
:
'required'
,
search
:
true
},
logoText
:
true
,
...
...
server/graph/resolvers/asset.js
View file @
4cdeba80
...
...
@@ -2,7 +2,9 @@ const _ = require('lodash')
const
sanitize
=
require
(
'sanitize-filename'
)
const
graphHelper
=
require
(
'../../helpers/graph'
)
const
assetHelper
=
require
(
'../../helpers/asset'
)
const
{
setTimeout
}
=
require
(
'node:timers/promises'
)
const
path
=
require
(
'node:path'
)
const
fs
=
require
(
'fs-extra'
)
const
{
v4
:
uuid
}
=
require
(
'uuid'
)
module
.
exports
=
{
Query
:
{
...
...
@@ -187,10 +189,54 @@ module.exports = {
*/
async
uploadAssets
(
obj
,
args
,
context
)
{
try
{
const
results
=
await
Promise
.
allSettled
(
args
.
files
.
map
(
async
fl
=>
{
const
{
filename
,
mimetype
,
createReadStream
}
=
await
fl
WIKI
.
logger
.
debug
(
`Processing asset upload
${
filename
}
of type
${
mimetype
}
...`
)
if
(
!
WIKI
.
extensions
.
ext
.
sharp
.
isInstalled
)
{
throw
new
Error
(
'This feature requires the Sharp extension but it is not installed.'
)
}
if
(
!
[
'.png'
,
'.jpg'
,
'webp'
,
'.gif'
].
some
(
s
=>
filename
.
endsWith
(
s
)))
{
throw
new
Error
(
'Invalid File Extension. Must be svg, png, jpg, webp or gif.'
)
}
const
destFormat
=
mimetype
.
startsWith
(
'image/svg'
)
?
'svg'
:
'png'
const
destFolder
=
path
.
resolve
(
process
.
cwd
(),
WIKI
.
config
.
dataPath
,
`assets`
)
const
destPath
=
path
.
join
(
destFolder
,
`logo-
${
args
.
id
}
.
${
destFormat
}
`
)
await
fs
.
ensureDir
(
destFolder
)
// -> Resize
await
WIKI
.
extensions
.
ext
.
sharp
.
resize
({
format
:
destFormat
,
inputStream
:
createReadStream
(),
outputPath
:
destPath
,
height
:
72
})
// -> Save logo meta to DB
const
site
=
await
WIKI
.
db
.
sites
.
query
().
findById
(
args
.
id
)
if
(
!
site
.
config
.
assets
.
logo
)
{
site
.
config
.
assets
.
logo
=
uuid
()
}
site
.
config
.
assets
.
logoExt
=
destFormat
await
WIKI
.
db
.
sites
.
query
().
findById
(
args
.
id
).
patch
({
config
:
site
.
config
})
await
WIKI
.
db
.
sites
.
reloadCache
()
// -> Save image data to DB
const
imgBuffer
=
await
fs
.
readFile
(
destPath
)
await
WIKI
.
db
.
knex
(
'assetData'
).
insert
({
id
:
site
.
config
.
assets
.
logo
,
data
:
imgBuffer
}).
onConflict
(
'id'
).
merge
()
}))
WIKI
.
logger
.
debug
(
'Asset(s) uploaded successfully.'
)
return
{
operation
:
graphHelper
.
generateSuccess
(
'Asset(s) uploaded successfully
.
'
)
operation
:
graphHelper
.
generateSuccess
(
'Asset(s) uploaded successfully'
)
}
}
catch
(
err
)
{
WIKI
.
logger
.
warn
(
err
)
return
graphHelper
.
generateError
(
err
)
}
},
...
...
server/graph/schemas/page.graphql
View file @
4cdeba80
...
...
@@ -103,7 +103,7 @@ extend type Mutation {
):
PageResponse
convertPage
(
id
:
Int
!
id
:
UUID
!
editor
:
String
!
):
DefaultResponse
...
...
@@ -114,7 +114,7 @@ extend type Mutation {
):
DefaultResponse
deletePage
(
id
:
Int
!
id
:
UUID
!
):
DefaultResponse
deleteTag
(
...
...
server/graph/schemas/site.graphql
View file @
4cdeba80
...
...
@@ -78,10 +78,11 @@ type SiteRobots {
type
SiteFeatures
{
ratings
:
Boolean
ratingsMode
:
SitePageRatingMode
s
ratingsMode
:
SitePageRatingMode
comments
:
Boolean
contributions
:
Boolean
profile
:
Boolean
reasonForChange
:
SiteReasonForChangeMode
search
:
Boolean
}
...
...
@@ -129,12 +130,18 @@ enum SiteThemePosition {
right
}
enum
SitePageRatingMode
s
{
enum
SitePageRatingMode
{
off
thumbs
stars
}
enum
SiteReasonForChangeMode
{
off
optional
required
}
type
SiteCreateResponse
{
operation
:
Operation
site
:
Site
...
...
@@ -164,10 +171,11 @@ input SiteRobotsInput {
input
SiteFeaturesInput
{
ratings
:
Boolean
ratingsMode
:
SitePageRatingMode
s
ratingsMode
:
SitePageRatingMode
comments
:
Boolean
contributions
:
Boolean
profile
:
Boolean
reasonForChange
:
SiteReasonForChangeMode
search
:
Boolean
}
...
...
server/views/base.pug
View file @
4cdeba80
...
...
@@ -40,20 +40,28 @@ html(lang=siteConfig.lang)
//- CSS
link(
type='text/css'
rel='stylesheet'
href='/_assets-legacy/css/app.36b4c9522aa279325701.css'
)
//- JS
script(
type='text/javascript'
src='/_assets-legacy/js/runtime.js'
src='/_assets-legacy/js/runtime.js
?1671237890
'
)
script(
type='text/javascript'
src='/_assets-legacy/js/app.js'
src='/_assets-legacy/js/app.js
?1671237890
'
)
...
...
ux/public/_assets/icons/fluent-save-as.svg
0 → 100644
View file @
4cdeba80
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 48 48"
width=
"96px"
height=
"96px"
><linearGradient
id=
"ejEliz0oBcrSlcUFYPkYAa"
x1=
"5.715"
x2=
"40.857"
y1=
"40.37"
y2=
"5.229"
gradientTransform=
"matrix(1 0 0 -1 0 48)"
gradientUnits=
"userSpaceOnUse"
><stop
offset=
"0"
stop-color=
"#32bdef"
/><stop
offset=
"1"
stop-color=
"#1ea2e4"
/></linearGradient><path
fill=
"url(#ejEliz0oBcrSlcUFYPkYAa)"
d=
"M36.2,6H8C6.9,6,6,6.9,6,8v32c0,1.1,0.9,2,2,2h32c1.1,0,2-0.9,2-2V11.8c0-0.5-0.2-1-0.6-1.4l-3.8-3.8 C37.2,6.2,36.7,6,36.2,6z"
/><linearGradient
id=
"ejEliz0oBcrSlcUFYPkYAb"
x1=
"38.003"
x2=
"40.027"
y1=
"9.997"
y2=
"7.973"
gradientTransform=
"matrix(1 0 0 -1 0 48)"
gradientUnits=
"userSpaceOnUse"
><stop
offset=
"0"
stop-color=
"#0d61a9"
/><stop
offset=
"1"
stop-color=
"#16528c"
/></linearGradient><rect
width=
"2"
height=
"2"
x=
"38"
y=
"38"
fill=
"url(#ejEliz0oBcrSlcUFYPkYAb)"
/><linearGradient
id=
"ejEliz0oBcrSlcUFYPkYAc"
x1=
"8.003"
x2=
"10.027"
y1=
"9.997"
y2=
"7.973"
gradientTransform=
"matrix(1 0 0 -1 0 48)"
gradientUnits=
"userSpaceOnUse"
><stop
offset=
"0"
stop-color=
"#0d61a9"
/><stop
offset=
"1"
stop-color=
"#16528c"
/></linearGradient><rect
width=
"2"
height=
"2"
x=
"8"
y=
"38"
fill=
"url(#ejEliz0oBcrSlcUFYPkYAc)"
/><linearGradient
id=
"ejEliz0oBcrSlcUFYPkYAd"
x1=
"13.83"
x2=
"34.965"
y1=
"23.83"
y2=
"44.965"
gradientUnits=
"userSpaceOnUse"
><stop
offset=
"0"
stop-color=
"#fff"
/><stop
offset=
".242"
stop-color=
"#f2f2f2"
/><stop
offset=
"1"
stop-color=
"#ccc"
/></linearGradient><path
fill=
"url(#ejEliz0oBcrSlcUFYPkYAd)"
d=
"M34,42H14c-1.1,0-2-0.9-2-2V28c0-1.1,0.9-2,2-2h20c1.1,0,2,0.9,2,2v12C36,41.1,35.1,42,34,42z"
/><path
d=
"M42,15.6L26.1,31.5l-1.2,4.9c-0.1,0.4,0.3,0.8,0.7,0.7l4.9-1.2L42,24.4V15.6z"
opacity=
".05"
/><path
d=
"M42,16.3L26.9,31.4l-0.5,1l0,0h0l-0.9,3.4c-0.1,0.4,0.3,0.7,0.6,0.6l3.4-0.9l0,0l0,0l1-0.5L42,23.7V16.3z"
opacity=
".07"
/><path
fill=
"#c94f60"
d=
"M45.8,18.1l-1.9-1.9c-0.3-0.3-0.8-0.3-1.1,0l-0.9,0.9l3,3l0.9-0.9C46.1,18.9,46.1,18.4,45.8,18.1"
/><path
fill=
"#f0f0f0"
d=
"M27,32l-1,4l4-1l0.4-3.5L27,32z"
/><path
fill=
"#edbe00"
d=
"M42.3,22.6L30,35l-3-3l12.3-12.3L42.3,22.6z"
/><linearGradient
id=
"ejEliz0oBcrSlcUFYPkYAe"
x1=
"42.112"
x2=
"42.112"
y1=
"30.688"
y2=
"25.199"
gradientTransform=
"matrix(1 0 0 -1 0 48)"
gradientUnits=
"userSpaceOnUse"
><stop
offset=
"0"
stop-color=
"#dedede"
/><stop
offset=
"1"
stop-color=
"#d6d6d6"
/></linearGradient><path
fill=
"url(#ejEliz0oBcrSlcUFYPkYAe)"
d=
"M39.3,19.7l2.5-2.5l3,3l-2.5,2.5L39.3,19.7z"
/><path
fill=
"#787878"
d=
"M26.5,34L26,36l2-0.5L26.5,34z"
/><path
fill=
"#1fa4e6"
d=
"M33,19H13c-1.1,0-2-0.9-2-2V6h24v11C35,18.1,34.1,19,33,19z"
/><path
d=
"M33,18.5H17c-1.4,0-2.5-1.1-2.5-2.5V6h21v10C35.5,17.4,34.4,18.5,33,18.5z"
opacity=
".07"
/><radialGradient
id=
"ejEliz0oBcrSlcUFYPkYAf"
cx=
"17.573"
cy=
"45.392"
r=
"23.87"
gradientTransform=
"matrix(1 0 0 -1 0 48)"
gradientUnits=
"userSpaceOnUse"
><stop
offset=
"0"
stop-color=
"#fafafb"
/><stop
offset=
".523"
stop-color=
"#e2e4e7"
/><stop
offset=
"1"
stop-color=
"#c8cdd1"
/></radialGradient><path
fill=
"url(#ejEliz0oBcrSlcUFYPkYAf)"
d=
"M15,6v10c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V6H15z"
/><rect
width=
"4"
height=
"8"
x=
"27"
y=
"8"
fill=
"#177cad"
/></svg>
\ No newline at end of file
ux/public/_assets/icons/ultraviolet-confusion.svg
0 → 100644
View file @
4cdeba80
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 40 40"
width=
"80px"
height=
"80px"
><path
fill=
"#fff"
d=
"M13.5,38.5v-5H12c-2.481,0-4.5-2.019-4.5-4.5v-5.141l-2.512-0.932 c-0.197-0.073-0.349-0.22-0.428-0.413c-0.08-0.194-0.074-0.406,0.015-0.596L7.5,15.705v-0.111c0-3.502,0.904-6.11,2.687-7.751 l0.376-0.346l-0.354-0.369C9.469,6.357,8.94,5.583,8.663,5.14C9.93,4.276,14.53,1.5,21.268,1.5C28.982,1.5,35.5,7.867,35.5,15.403 c0,5.646-2.612,8.415-4.917,10.856c-1.352,1.433-2.628,2.785-3.067,4.505L27.5,30.826V38.5H13.5z"
/><path
fill=
"#4788c7"
d=
"M21.268,2C28.711,2,35,8.138,35,15.403c0,5.448-2.54,8.139-4.781,10.514 c-1.397,1.481-2.717,2.879-3.188,4.724L27,30.763v0.126V38H14v-4v-1h-1h-1c-2.206,0-4-1.794-4-4v-4.793v-0.696l-0.652-0.242 l-2.185-0.81c-0.082-0.03-0.121-0.09-0.139-0.134s-0.032-0.114,0.005-0.193l2.877-6.112L8,15.817v-0.224 c0-3.356,0.85-5.84,2.526-7.383l0.752-0.692L10.57,6.783c-0.518-0.54-0.928-1.082-1.215-1.499C10.99,4.255,15.259,2,21.268,2 M21.268,1C13.043,1,8,5,8,5s0.657,1.234,1.849,2.475C8.132,9.055,7,11.635,7,15.594l-2.877,6.112 c-0.309,0.657,0.01,1.439,0.691,1.691L7,24.207V29c0,2.761,2.239,5,5,5h1v5h15v-8.111c1.105-4.326,8-6.228,8-15.486 C36,7.449,29.223,1,21.268,1L21.268,1z"
/><g><path
fill=
"#98ccfd"
d=
"M18.548,23.841c-0.073-0.203-0.28-0.922-0.28-1.756c0-2.916,4.013-4.213,4.013-6.565 c0-2.044-2.383-2.154-2.757-2.154c-1.212,0-2.366,0.541-3.463,1.622v-2.817C17.393,11.39,18.775,11,20.206,11 c3.955,0,4.817,2.557,4.817,3.988c0,3.792-4.305,4.749-4.305,7.183c0,0.762,0.313,1.476,0.402,1.671H18.548z M19.938,29 c-0.79,0-1.744-0.566-1.744-1.622c0-1.163,1.125-1.634,1.744-1.634c1.168,0,1.732,0.953,1.732,1.634 C21.67,28.424,20.654,29,19.938,29z"
/></g></svg>
\ No newline at end of file
ux/public/_assets/icons/ultraviolet-new-document.svg
0 → 100644
View file @
4cdeba80
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 40 40"
width=
"80px"
height=
"80px"
><path
fill=
"#fff"
d=
"M6,36.5c-1.379,0-2.5-1.121-2.5-2.5V6c0-1.379,1.121-2.5,2.5-2.5h28c1.379,0,2.5,1.121,2.5,2.5v28 c0,1.379-1.121,2.5-2.5,2.5H6z"
/><path
fill=
"#4788c7"
d=
"M34,4c1.103,0,2,0.897,2,2v28c0,1.103-0.897,2-2,2H6c-1.103,0-2-0.897-2-2V6c0-1.103,0.897-2,2-2 H34 M34,3H6C4.343,3,3,4.343,3,6v28c0,1.657,1.343,3,3,3h28c1.657,0,3-1.343,3-3V6C37,4.343,35.657,3,34,3L34,3z"
/><path
fill=
"#4788c7"
d=
"M28.5 13h-17c-.276 0-.5-.224-.5-.5v0c0-.276.224-.5.5-.5h17c.276 0 .5.224.5.5v0C29 12.776 28.776 13 28.5 13zM23.5 17h-12c-.276 0-.5-.224-.5-.5v0c0-.276.224-.5.5-.5h12c.276 0 .5.224.5.5v0C24 16.776 23.776 17 23.5 17zM28.5 21h-17c-.276 0-.5-.224-.5-.5v0c0-.276.224-.5.5-.5h17c.276 0 .5.224.5.5v0C29 20.776 28.776 21 28.5 21zM28.5 29h-17c-.276 0-.5-.224-.5-.5v0c0-.276.224-.5.5-.5h17c.276 0 .5.224.5.5v0C29 28.776 28.776 29 28.5 29zM23.385 25h-12c-.276 0-.5-.224-.5-.5l0 0c0-.276.224-.5.5-.5h12c.276 0 .5.224.5.5v0C23.885 24.776 23.661 25 23.385 25z"
/></svg>
\ No newline at end of file
ux/src/components/FileManager.vue
View file @
4cdeba80
...
...
@@ -44,6 +44,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
@lazy-load='treeLazyLoad'
:use-lazy-load='true'
@context-action='treeContextAction'
:display-mode='state.displayMode'
)
q-drawer.fileman-right(:model-value='true', :width='350', side='right')
.q-pa-md
...
...
@@ -90,7 +91,6 @@ q-layout.fileman(view='hHh lpR lFr', container)
)
q-tooltip(anchor='bottom middle', self='top middle')
{{
t
(
`fileman.viewOptions`
)
}}
q-menu(
auto-close
transition-show='jump-down'
transition-hide='jump-up'
anchor='bottom right'
...
...
@@ -103,11 +103,38 @@ q-layout.fileman(view='hHh lpR lFr', container)
q-separator.q-my-sm
q-item(clickable)
q-item-section(side)
q-icon(name='las la-circle', color='grey', size='xs')
q-icon(name='las la-list', color='grey', size='xs')
q-item-section.q-pr-sm Browse using...
q-item-section(side)
q-icon(name='las la-angle-right', color='grey', size='xs')
q-menu(
anchor='top end'
self='top start'
)
q-list.q-pa-sm(dense)
q-item(clickable, @click='state.displayMode = `path`')
q-item-section(side)
q-icon(
:name='state.displayMode === `path` ? `las la-check-circle` : `las la-circle`'
:color='state.displayMode === `path` ? `positive` : `grey`'
size='xs'
)
q-item-section.q-pr-sm Browse Using Paths
q-item(clickable, @click='state.displayMode = `title`')
q-item-section(side)
q-icon(
:name='state.displayMode === `title` ? `las la-check-circle` : `las la-circle`'
:color='state.displayMode === `title` ? `positive` : `grey`'
size='xs'
)
q-item-section.q-pr-sm Browse Using Titles
q-item(clickable)
q-item-section(side)
q-icon(name='las la-stop', color='grey', size='xs')
q-item-section.q-pr-sm Compact List
q-item(clickable)
q-item-section(side)
q-icon(name='las la-check-
circl
e', color='positive', size='xs')
q-icon(name='las la-check-
squar
e', color='positive', size='xs')
q-item-section.q-pr-sm Show Folders
q-btn.q-mr-sm(
flat
...
...
@@ -254,6 +281,7 @@ const state = reactive({
currentFileId
:
''
,
treeNodes
:
{},
treeRoots
:
[],
displayMode
:
'title'
,
isUploading
:
false
,
shouldCancelUpload
:
false
,
uploadPercentage
:
0
,
...
...
@@ -450,6 +478,7 @@ async function loadTree (parentId, types) {
case
'TreeItemFolder'
:
{
state
.
treeNodes
[
item
.
id
]
=
{
text
:
item
.
title
,
fileName
:
item
.
fileName
,
children
:
[]
}
if
(
!
item
.
folderPath
)
{
...
...
ux/src/components/PageBrowser.vue
deleted
100644 → 0
View file @
2a051637
<
template
lang=
"pug"
>
.row
.col-auto.bg-grey-1(style='width: 250px;')
q-tree(
:nodes='tree'
default-expand-all
node-key='label'
@lazy-load='onLazyLoad'
)
.col Doude
</
template
>
<
script
>
export
default
{
data
()
{
return
{
tree
:
[
{
label
:
'Item 1'
,
icon
:
'las la-folder'
,
children
:
[
{
label
:
'Item 1.1'
},
{
label
:
'Item 1.2'
,
icon
:
'las la-folder'
,
children
:
[
{
label
:
'Item 1.2.1'
},
{
label
:
'Item 1.2.2'
}
]
}
]
}
]
}
},
methods
:
{
async
onLazyLoad
({
node
,
key
,
done
,
fail
})
{
done
([])
}
}
}
</
script
>
ux/src/components/PageDeleteDialog.vue
0 → 100644
View file @
4cdeba80
<
template
lang=
"pug"
>
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 550px; max-width: 850px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm')
span
{{
t
(
`pageDeleteDialog.title`
)
}}
q-card-section
.text-body2
i18n-t(keypath='pageDeleteDialog.confirm')
template(v-slot:name)
strong
{{
pageName
}}
.text-caption.text-grey.q-mt-sm
{{
t
(
'pageDeleteDialog.pageId'
,
{
id
:
pageId
}
)
}}
q
-
card
-
actions
.
card
-
actions
q
-
space
q
-
btn
.
acrylic
-
btn
(
flat
:
label
=
't(`common.actions.cancel`)'
color
=
'grey'
padding
=
'xs md'
@
click
=
'onDialogCancel'
)
q
-
btn
(
unelevated
:
label
=
't(`common.actions.delete`)'
color
=
'negative'
padding
=
'xs md'
@
click
=
'confirm'
:
loading
=
'state.isLoading'
)
<
/template
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
{
useI18n
}
from
'vue-i18n'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
reactive
}
from
'vue'
// PROPS
const
props
=
defineProps
({
pageId
:
{
type
:
String
,
required
:
true
}
,
pageName
:
{
type
:
String
,
required
:
true
}
}
)
// EMITS
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
isLoading
:
false
}
)
// METHODS
async
function
confirm
()
{
state
.
isLoading
=
true
try
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation deletePage ($id: UUID!) {
deletePage(id: $id) {
operation {
succeeded
message
}
}
}
`
,
variables
:
{
id
:
props
.
pageId
}
}
)
if
(
resp
?.
data
?.
deletePage
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'pageDeleteDialog.deleteSuccess'
)
}
)
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
deletePage
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
}
)
}
state
.
isLoading
=
false
}
<
/script
>
ux/src/components/PageSaveDialog.vue
View file @
4cdeba80
<
template
lang=
"pug"
>
q-card.page-save-dialog(style='width: 860px; max-width: 90vw;')
q-toolbar.bg-primary.text-white
.text-subtitle2
{{
$t
(
'editor.pageSave.title'
)
}}
page-browser
q-card-section
q-input(
v-model='reason'
label='Reason for change'
dense
outlined
)
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
icon='las la-times'
:label='$t(`common.actions.cancel`)'
color='grey-7'
padding='xs md'
v-close-popup
flat
)
q-btn(
icon='las la-check'
:label='$t(`common.actions.save`)'
unelevated
color='primary'
padding='xs md'
@click=''
v-close-popup
)
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card.page-save-dialog(style='width: 860px; max-width: 90vw;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-save-as.svg', left, size='sm')
span
{{
t
(
'pageSaveDialog.title'
)
}}
.row.page-save-dialog-browser
.col-4.q-px-sm
tree(
:nodes='state.treeNodes'
:roots='state.treeRoots'
v-model:selected='state.currentFolderId'
@lazy-load='treeLazyLoad'
:use-lazy-load='true'
@context-action='treeContextAction'
:context-action-list='[`newFolder`]'
:display-mode='state.displayMode'
)
.col-8
q-list.page-save-dialog-filelist(dense)
q-item(
v-for='item of files'
:key='item.id'
clickable
active-class='active'
:active='item.id === state.currentFileId'
@click.native='state.currentFileId = item.id'
@dblclick.native='openItem(item)'
)
q-item-section(side)
q-icon(:name='item.icon', size='sm')
q-item-section
q-item-label
{{
item
.
title
}}
q-list.q-py-sm
q-item
blueprint-icon(icon='new-document')
q-item-section
q-input(
v-model='state.title'
label='Page Title'
dense
outlined
)
q-item
blueprint-icon(icon='file-submodule')
q-item-section
q-input(
v-model='state.path'
label='Path Name'
dense
outlined
)
q-card-actions.card-actions.q-px-md
q-btn.acrylic-btn(
icon='las la-ellipsis-h'
color='blue-grey'
padding='xs sm'
flat
)
q-tooltip(anchor='center right' self='center left') Display Options
q-menu(
auto-close
transition-show='jump-down'
transition-hide='jump-up'
anchor='top left'
self='bottom left'
)
q-card.q-pa-sm
q-list(dense)
q-item(clickable, @click='state.displayMode = `path`')
q-item-section(side)
q-icon(
:name='state.displayMode === `path` ? `las la-check-circle` : `las la-circle`'
:color='state.displayMode === `path` ? `positive` : `grey`'
size='xs'
)
q-item-section.q-pr-sm Browse Using Paths
q-item(clickable, @click='state.displayMode = `title`')
q-item-section(side)
q-icon(
:name='state.displayMode === `title` ? `las la-check-circle` : `las la-circle`'
:color='state.displayMode === `title` ? `positive` : `grey`'
size='xs'
)
q-item-section.q-pr-sm Browse Using Titles
q-space
q-btn.acrylic-btn(
icon='las la-times'
:label='t(`common.actions.cancel`)'
color='grey-7'
padding='xs md'
@click='onDialogCancel'
flat
)
q-btn(
icon='las la-check'
:label='t(`common.actions.save`)'
unelevated
color='primary'
padding='xs md'
@click='save'
v-close-popup
)
</
template
>
<
script
>
import
PageBrowser
from
'./PageBrowser.vue'
<
script
setup
>
import
{
useI18n
}
from
'vue-i18n'
import
{
computed
,
onMounted
,
reactive
}
from
'vue'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
cloneDeep
,
find
}
from
'lodash-es'
import
gql
from
'graphql-tag'
export
default
{
components
:
{
PageBrowser
},
data
()
{
return
{
reason
:
''
}
import
fileTypes
from
'../helpers/fileTypes'
import
FolderCreateDialog
from
'src/components/FolderCreateDialog.vue'
import
Tree
from
'src/components/TreeNav.vue'
import
{
usePageStore
}
from
'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
// PROPS
const
props
=
defineProps
({
mode
:
{
type
:
String
,
required
:
false
,
default
:
'save'
},
computed
:
{
pageId
:
{
type
:
String
,
required
:
true
},
mounted
()
{
pageName
:
{
type
:
String
,
required
:
false
,
default
:
''
},
methods
:
{
pagePath
:
{
type
:
String
,
required
:
false
,
default
:
''
}
})
// EMITS
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// STORES
const
pageStore
=
usePageStore
()
const
siteStore
=
useSiteStore
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
displayMode
:
'title'
,
currentFolderId
:
''
,
currentFileId
:
''
,
treeNodes
:
{},
treeRoots
:
[],
fileList
:
[
{
id
:
'1'
,
type
:
'folder'
,
title
:
'Beep Boop'
},
{
id
:
'2'
,
type
:
'folder'
,
title
:
'Second Folder'
},
{
id
:
'3'
,
type
:
'page'
,
title
:
'Some Page'
,
pageType
:
'markdown'
}
],
title
:
''
,
path
:
''
})
const
displayModes
=
[
{
value
:
'title'
,
label
:
t
(
'pageSaveDialog.displayModeTitle'
)
},
{
value
:
'path'
,
label
:
t
(
'pageSaveDialog.displayModePath'
)
}
]
// COMPUTED
const
files
=
computed
(()
=>
{
return
state
.
fileList
.
map
(
f
=>
{
switch
(
f
.
type
)
{
case
'folder'
:
{
f
.
icon
=
fileTypes
.
folder
.
icon
break
}
case
'page'
:
{
f
.
icon
=
fileTypes
.
page
.
icon
break
}
}
return
f
})
})
// METHODS
async
function
save
()
{
onDialogOK
()
}
async
function
treeLazyLoad
(
nodeId
,
{
done
,
fail
})
{
await
loadTree
(
nodeId
,
[
'folder'
,
'page'
])
done
()
}
async
function
loadTree
(
parentId
,
types
)
{
try
{
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query loadTree (
$siteId: UUID!
$parentId: UUID
$types: [TreeItemType]
) {
tree (
siteId: $siteId
parentId: $parentId
types: $types
) {
__typename
... on TreeItemFolder {
id
folderPath
fileName
title
childrenCount
}
... on TreeItemPage {
id
folderPath
fileName
title
createdAt
updatedAt
pageEditor
}
}
}
`
,
variables
:
{
siteId
:
siteStore
.
id
,
parentId
,
types
},
fetchPolicy
:
'network-only'
})
const
items
=
cloneDeep
(
resp
?.
data
?.
tree
)
if
(
items
?.
length
>
0
)
{
const
newTreeRoots
=
[]
for
(
const
item
of
items
)
{
switch
(
item
.
__typename
)
{
case
'TreeItemFolder'
:
{
state
.
treeNodes
[
item
.
id
]
=
{
text
:
item
.
title
,
fileName
:
item
.
fileName
,
children
:
[]
}
if
(
!
item
.
folderPath
)
{
newTreeRoots
.
push
(
item
.
id
)
}
else
{
state
.
treeNodes
[
parentId
].
children
.
push
(
item
.
id
)
}
break
}
}
}
if
(
newTreeRoots
.
length
>
0
)
{
state
.
treeRoots
=
newTreeRoots
}
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
'Failed to load folder tree.'
,
caption
:
err
.
message
})
}
}
function
treeContextAction
(
nodeId
,
action
)
{
switch
(
action
)
{
case
'newFolder'
:
{
newFolder
(
nodeId
)
break
}
}
}
function
newFolder
(
parentId
)
{
$q
.
dialog
({
component
:
FolderCreateDialog
,
componentProps
:
{
parentId
}
}).
onOk
(()
=>
{
loadTree
(
parentId
)
})
}
// MOUNTED
onMounted
(()
=>
{
loadTree
()
state
.
title
=
props
.
pageName
||
''
state
.
path
=
props
.
pagePath
||
''
})
</
script
>
<
style
lang=
"scss"
>
.page-save-dialog
{
&
-browser
{
height
:
300px
;
max-height
:
90vh
;
border-bottom
:
1px
solid
$blue-grey-1
;
>
.col-4
{
@at-root
.body--light
&
{
background-color
:
$blue-grey-1
;
border-bottom-color
:
$blue-grey-1
;
}
@at-root
.body--dark
&
{
background-color
:
$dark-4
;
border-bottom-color
:
$dark-4
;
}
}
}
&
-filelist
{
padding
:
8px
12px
;
>
.q-item
{
padding
:
4px
6px
;
border-radius
:
4px
;
&
.active
{
background-color
:
var
(
--
q-primary
);
color
:
#FFF
;
.fileman-filelist-label
.q-item__label--caption
{
color
:
rgba
(
255
,
255
,
255
,.
7
);
}
.fileman-filelist-side
.text-caption
{
color
:
rgba
(
255
,
255
,
255
,.
7
);
}
}
}
}
}
</
style
>
ux/src/components/TreeLevel.vue
View file @
4cdeba80
...
...
@@ -6,6 +6,7 @@ ul.treeview-level
q-icon(name='img:/_assets/icons/fluent-ftp.svg', size='sm')
.treeview-label-text(:class='$q.dark.isActive ? `text-purple-4` : `text-purple`') root
q-menu(
v-if='rootContextActionList.length > 0'
touch-position
context-menu
auto-close
...
...
@@ -14,10 +15,15 @@ ul.treeview-level
)
q-card.q-pa-sm
q-list(dense, style='min-width: 150px;')
q-item(clickable, @click='createRootFolder')
q-item(
v-for='action of rootContextActionList'
:key='action.key'
clickable
@click='action.handler(null)'
)
q-item-section(side)
q-icon(
name='las la-plus-circle', color='primary
')
q-item-section
New Folder
q-icon(
:name='action.icon', :color='action.iconColor
')
q-item-section
(:class='action.labelColor && (`text-` + action.labelColor)')
{{
action
.
label
}}
q-icon(
v-if='!selection'
name='las la-angle-right'
...
...
@@ -62,9 +68,15 @@ const roots = inject('roots')
const
nodes
=
inject
(
'nodes'
)
const
selection
=
inject
(
'selection'
)
const
emitContextAction
=
inject
(
'emitContextAction'
)
const
contextActionList
=
inject
(
'contextActionList'
)
// COMPUTED
const
rootContextActionList
=
computed
(()
=>
{
if
(
props
.
parentId
)
{
return
[]
}
return
contextActionList
.
filter
(
c
=>
c
.
key
===
'newFolder'
)
})
const
level
=
computed
(()
=>
{
const
items
=
[]
if
(
!
props
.
parentId
)
{
...
...
ux/src/components/TreeNav.vue
View file @
4cdeba80
...
...
@@ -7,6 +7,7 @@
</
template
>
<
script
setup
>
import
{
useI18n
}
from
'vue-i18n'
import
{
computed
,
onMounted
,
provide
,
reactive
,
toRef
}
from
'vue'
import
{
findKey
}
from
'lodash-es'
...
...
@@ -30,6 +31,14 @@ const props = defineProps({
useLazyLoad
:
{
type
:
Boolean
,
default
:
false
},
contextActionList
:
{
type
:
Array
,
default
:
()
=>
[
'newFolder'
,
'duplicate'
,
'rename'
,
'move'
,
'del'
]
},
displayMode
:
{
type
:
String
,
default
:
'title'
}
})
...
...
@@ -37,6 +46,48 @@ const props = defineProps({
const
emit
=
defineEmits
([
'update:selected'
,
'lazyLoad'
,
'contextAction'
])
// I18N
const
{
t
}
=
useI18n
()
// Context Actions
const
contextActions
=
{
newFolder
:
{
icon
:
'las la-plus-circle'
,
iconColor
:
'blue'
,
label
:
t
(
'common.actions.newFolder'
)
},
duplicate
:
{
icon
:
'las la-copy'
,
iconColor
:
'teal'
,
label
:
t
(
'common.actions.duplicate'
)
+
'...'
},
rename
:
{
icon
:
'las la-redo'
,
iconColor
:
'teal'
,
label
:
t
(
'common.actions.rename'
)
+
'...'
},
move
:
{
icon
:
'las la-arrow-right'
,
iconColor
:
'teal'
,
label
:
t
(
'common.actions.moveTo'
)
+
'...'
},
del
:
{
icon
:
'las la-trash-alt'
,
iconColor
:
'negative'
,
label
:
t
(
'common.actions.delete'
),
labelColor
:
'negative'
}
}
provide
(
'contextActionList'
,
props
.
contextActionList
.
map
(
key
=>
({
key
,
...
contextActions
[
key
],
handler
:
(
nodeId
)
=>
{
emit
(
'contextAction'
,
nodeId
,
key
)
}
})))
// DATA
const
state
=
reactive
({
...
...
@@ -75,6 +126,7 @@ provide('roots', toRef(props, 'roots'))
provide
(
'nodes'
,
props
.
nodes
)
provide
(
'loaded'
,
state
.
loaded
)
provide
(
'opened'
,
state
.
opened
)
provide
(
'displayMode'
,
toRef
(
props
,
'displayMode'
))
provide
(
'selection'
,
selection
)
provide
(
'emitLazyLoad'
,
emitLazyLoad
)
provide
(
'emitContextAction'
,
emitContextAction
)
...
...
ux/src/components/TreeNode.vue
View file @
4cdeba80
...
...
@@ -7,7 +7,7 @@ li.treeview-node
size='sm'
@click.stop='hasChildren ? toggleNode() : openNode()'
)
.treeview-label-text
{{
node
.
text
}}
.treeview-label-text
{{
displayMode
===
'path'
?
node
.
fileName
:
node
.
text
}}
q-spinner.q-mr-xs(
color='primary'
v-if='state.isLoading'
...
...
@@ -19,6 +19,7 @@ li.treeview-node
)
//- RIGHT-CLICK MENU
q-menu(
v-if='contextActionList.length > 0'
touch-position
context-menu
auto-close
...
...
@@ -29,26 +30,15 @@ li.treeview-node
)
q-card.q-pa-sm
q-list(dense, style='min-width: 150px;')
q-item(clickable, @click='contextAction(`newFolder`)')
q-item(
v-for='action of contextActionList'
:key='action.key'
clickable
@click='action.handler(node.id)'
)
q-item-section(side)
q-icon(name='las la-plus-circle', color='primary')
q-item-section New Folder
q-item(clickable)
q-item-section(side)
q-icon(name='las la-copy', color='teal')
q-item-section Duplicate...
q-item(clickable)
q-item-section(side)
q-icon(name='las la-redo', color='teal')
q-item-section Rename...
q-item(clickable)
q-item-section(side)
q-icon(name='las la-arrow-right', color='teal')
q-item-section Move to...
q-item(clickable)
q-item-section(side)
q-icon(name='las la-trash-alt', color='negative')
q-item-section.text-negative Delete
q-icon(:name='action.icon', :color='action.iconColor')
q-item-section(:class='action.labelColor && (`text-` + action.labelColor)')
{{
action
.
label
}}
//- SUB-LEVEL
transition(name='treeview')
tree-level(
...
...
@@ -89,9 +79,11 @@ const $q = useQuasar()
const
loaded
=
inject
(
'loaded'
)
const
opened
=
inject
(
'opened'
)
const
displayMode
=
inject
(
'displayMode'
)
const
selection
=
inject
(
'selection'
)
const
emitLazyLoad
=
inject
(
'emitLazyLoad'
)
const
emitContextAction
=
inject
(
'emitContextAction'
)
const
contextActionList
=
inject
(
'contextActionList'
)
// DATA
...
...
ux/src/i18n/locales/en.json
View file @
4cdeba80
...
...
@@ -1586,5 +1586,22 @@
"welcome.admin"
:
"Administration Area"
,
"welcome.createHome"
:
"Create the homepage"
,
"welcome.subtitle"
:
"Let's get started..."
,
"welcome.title"
:
"Welcome to Wiki.js!"
"welcome.title"
:
"Welcome to Wiki.js!"
,
"admin.utilities.scanPageProblems"
:
"Scan for Page Problems"
,
"admin.utilities.scanPageProblemsHint"
:
"Scan all pages for invalid, missing or corrupted data."
,
"pageSaveDialog.title"
:
"Save As..."
,
"admin.general.reasonForChange"
:
"Reason for Change"
,
"admin.general.reasonForChangeHint"
:
"Should users be prompted the reason for changes made to a page?"
,
"admin.general.reasonForChangeRequired"
:
"Required"
,
"admin.general.reasonForChangeOptional"
:
"Optional"
,
"admin.general.reasonForChangeOff"
:
"Off"
,
"pageDeleteDialog.title"
:
"Confirm Page Deletion"
,
"pageDeleteDialog.confirm"
:
"Are you sure you want to delete the page {name}?"
,
"pageDeleteDialog.pageId"
:
"Page ID {id}"
,
"pageDeleteDialog.deleteSuccess"
:
"Page deleted successfully."
,
"common.actions.newFolder"
:
"New Folder"
,
"common.actions.duplicate"
:
"Duplicate"
,
"common.actions.moveTo"
:
"Move To"
,
"pageSaveDialog.displayModeTitle"
:
"Title"
,
"pageSaveDialog.displayModePath"
:
"Path"
}
ux/src/pages/AdminGeneral.vue
View file @
4cdeba80
...
...
@@ -11,7 +11,7 @@ q-page.admin-general
icon='las la-question-circle'
flat
color='grey'
:href='siteStore.docsBase + `/admin/sites`'
:href='siteStore.docsBase + `/admin/sites
#general
`'
target='_blank'
type='a'
)
...
...
@@ -208,6 +208,21 @@ q-page.admin-general
unchecked-icon='las la-times'
:aria-label='t(`admin.general.allowSearch`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label')
blueprint-icon(icon='confusion')
q-item-section
q-item-label
{{
t
(
`admin.general.reasonForChange`
)
}}
q-item-label(caption)
{{
t
(
`admin.general.reasonForChangeHint`
)
}}
q-item-section(avatar)
q-btn-toggle(
v-model='state.config.features.reasonForChange'
push
glossy
no-caps
toggle-color='primary'
:options='reasonForChangeModes'
)
//- -----------------------
//- URL Handling
...
...
@@ -493,6 +508,7 @@ const state = reactive({
ratingsMode
:
'off'
,
comments
:
false
,
contributions
:
false
,
reasonForChange
:
'off'
,
profile
:
false
},
defaults
:
{
...
...
@@ -528,6 +544,11 @@ const ratingsModes = [
{
value
:
'thumbs'
,
label
:
t
(
'admin.general.ratingsThumbs'
)
},
{
value
:
'stars'
,
label
:
t
(
'admin.general.ratingsStars'
)
}
]
const
reasonForChangeModes
=
[
{
value
:
'off'
,
label
:
t
(
'admin.general.reasonForChangeOff'
)
},
{
value
:
'optional'
,
label
:
t
(
'admin.general.reasonForChangeOptional'
)
},
{
value
:
'required'
,
label
:
t
(
'admin.general.reasonForChangeRequired'
)
}
]
const
dateFormats
=
[
{
value
:
''
,
label
:
t
(
'profile.localeDefault'
)
},
{
value
:
'DD/MM/YYYY'
,
label
:
'DD/MM/YYYY'
},
...
...
@@ -586,10 +607,11 @@ async function load () {
}
features {
comments
ratings
ratingsMode
contributions
profile
ratings
ratingsMode
reasonForChange
search
}
defaults {
...
...
ux/src/pages/AdminUtilities.vue
View file @
4cdeba80
...
...
@@ -109,7 +109,19 @@ q-page.admin-utilities
@click=''
:label='t(`common.actions.proceed`)'
)
q-item
blueprint-icon(icon='rescan-document', :hue-rotate='45')
q-item-section
q-item-label
{{
t
(
`admin.utilities.scanPageProblems`
)
}}
q-item-label(caption)
{{
t
(
`admin.utilities.scanPageProblemsHint`
)
}}
q-item-section(side)
q-btn.acrylic-btn(
flat
icon='las la-arrow-circle-right'
color='primary'
@click=''
:label='t(`common.actions.proceed`)'
)
</
template
>
<
script
setup
>
...
...
ux/src/pages/Index.vue
View file @
4cdeba80
...
...
@@ -193,7 +193,7 @@ q-page.column
@click='state.tagEditMode = !state.tagEditMode'
)
page-tags.q-mt-sm(:edit='state.tagEditMode')
template(v-if='
pageStore.allowRatings && pageStore.ratingsMode !== `off`
')
template(v-if='
siteStore.features.ratingsMode !== `off` && pageStore.allowRatings
')
q-separator(v-if='pageStore.showToc || pageStore.showTags')
//- Rating
.q-pa-md.flex.items-center
...
...
@@ -201,13 +201,13 @@ q-page.column
.text-caption.text-grey-7 Rate this page
.q-px-md
q-rating(
v-if='
pageStore
.ratingsMode === `stars`'
v-if='
siteStore.features
.ratingsMode === `stars`'
v-model='state.currentRating'
icon='las la-star'
color='secondary'
size='sm'
)
.flex.items-center(v-else-if='
pageStore
.ratingsMode === `thumbs`')
.flex.items-center(v-else-if='
siteStore.features
.ratingsMode === `thumbs`')
q-btn.acrylic-btn(
flat
icon='las la-thumbs-down'
...
...
@@ -239,24 +239,11 @@ q-page.column
q-separator.q-my-sm(inset)
q-btn.q-py-sm(
flat
icon='las la-history'
color='grey'
aria-label='Page History'
)
q-tooltip(anchor='center left' self='center right') Page History
q-btn.q-py-sm(
flat
icon='las la-code'
color='grey'
aria-label='Page Source'
)
q-tooltip(anchor='center left' self='center right') Page Source
q-btn.q-py-sm(
flat
icon='las la-ellipsis-h'
color='grey'
aria-label='Page Actions'
)
q-tooltip(anchor='center left' self='center right') Page Actions
q-menu(
anchor='top left'
self='top right'
...
...
@@ -266,6 +253,16 @@ q-page.column
q-list(padding, style='min-width: 225px;')
q-item(clickable)
q-item-section.items-center(avatar)
q-icon(color='deep-orange-9', name='las la-history', size='sm')
q-item-section
q-item-label View History
q-item(clickable)
q-item-section.items-center(avatar)
q-icon(color='deep-orange-9', name='las la-code', size='sm')
q-item-section
q-item-label View Source
q-item(clickable)
q-item-section.items-center(avatar)
q-icon(color='deep-orange-9', name='las la-atom', size='sm')
q-item-section
q-item-label Convert Page
...
...
@@ -285,6 +282,7 @@ q-page.column
icon='las la-copy'
color='grey'
aria-label='Duplicate Page'
@click='duplicatePage'
)
q-tooltip(anchor='center left' self='center right') Duplicate Page
q-btn.q-py-sm(
...
...
@@ -292,6 +290,7 @@ q-page.column
icon='las la-share'
color='grey'
aria-label='Rename / Move Page'
@click='renamePage'
)
q-tooltip(anchor='center left' self='center right') Rename / Move Page
q-btn.q-py-sm(
...
...
@@ -299,7 +298,7 @@ q-page.column
icon='las la-trash'
color='grey'
aria-label='Delete Page'
@click='
sav
ePage'
@click='
delet
ePage'
)
q-tooltip(anchor='center left' self='center right') Delete Page
...
...
@@ -313,13 +312,6 @@ q-page.column
no-shake
)
component(:is='sideDialogs[state.sideDialogComponent]')
q-dialog(
v-model='state.showGlobalDialog'
transition-show='jump-up'
transition-hide='jump-down'
)
component(:is='globalDialogs[state.globalDialogComponent]')
</
template
>
<
script
setup
>
...
...
@@ -349,9 +341,6 @@ const sideDialogs = {
loadingComponent
:
LoadingGeneric
})
}
const
globalDialogs
=
{
PageSaveDialog
:
defineAsyncComponent
(()
=>
import
(
'../components/PageSaveDialog.vue'
))
}
// QUASAR
...
...
@@ -472,9 +461,44 @@ function togglePageData () {
state
.
showSideDialog
=
true
}
function
savePage
()
{
state
.
globalDialogComponent
=
'PageSaveDialog'
state
.
showGlobalDialog
=
true
function
duplicatePage
()
{
$q
.
dialog
({
component
:
defineAsyncComponent
(()
=>
import
(
'../components/PageSaveDialog.vue'
)),
componentProps
:
{
mode
:
'duplicate'
,
pageId
:
pageStore
.
id
,
pageName
:
pageStore
.
title
,
pagePath
:
pageStore
.
path
}
}).
onOk
(()
=>
{
// TODO: change route to new location
})
}
function
renamePage
()
{
$q
.
dialog
({
component
:
defineAsyncComponent
(()
=>
import
(
'../components/PageSaveDialog.vue'
)),
componentProps
:
{
mode
:
'rename'
,
pageId
:
pageStore
.
id
,
pageName
:
pageStore
.
title
,
pagePath
:
pageStore
.
path
}
}).
onOk
(()
=>
{
// TODO: change route to new location
})
}
function
deletePage
()
{
$q
.
dialog
({
component
:
defineAsyncComponent
(()
=>
import
(
'../components/PageDeleteDialog.vue'
)),
componentProps
:
{
pageId
:
pageStore
.
id
,
pageName
:
pageStore
.
title
}
}).
onOk
(()
=>
{
router
.
replace
(
'/'
)
})
}
function
refreshTocExpanded
(
baseToc
,
lvl
)
{
...
...
ux/src/stores/site.js
View file @
4cdeba80
...
...
@@ -21,11 +21,13 @@ export const useSiteStore = defineStore('site', {
searchRestrictLocale
:
false
,
searchRestrictPath
:
false
,
printView
:
false
,
ratingsMode
:
'thumbs'
,
pageDataTemplates
:
[],
showSideNav
:
true
,
showSidebar
:
true
,
overlay
:
null
,
features
:
{
ratingsMode
:
'off'
},
theme
:
{
dark
:
false
,
injectCSS
:
''
,
...
...
@@ -76,6 +78,9 @@ export const useSiteStore = defineStore('site', {
company
contentLicense
footerExtra
features {
ratingsMode
}
theme {
dark
colorPrimary
...
...
@@ -107,6 +112,10 @@ export const useSiteStore = defineStore('site', {
this
.
company
=
clone
(
siteInfo
.
company
)
this
.
contentLicense
=
clone
(
siteInfo
.
contentLicense
)
this
.
footerExtra
=
clone
(
siteInfo
.
footerExtra
)
this
.
features
=
{
...
this
.
features
,
...
clone
(
siteInfo
.
features
)
}
this
.
theme
=
{
...
this
.
theme
,
...
clone
(
siteInfo
.
theme
)
...
...
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