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
b4769b9a
You need to sign in or sign up before continuing.
Unverified
Commit
b4769b9a
authored
Dec 24, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: File Manager improvements + system flags
parent
4cdeba80
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
620 additions
and
205 deletions
+620
-205
data.yml
server/app/data.yml
+3
-2
config.js
server/core/config.js
+1
-1
3.0.0.js
server/db/migrations/3.0.0.js
+10
-1
system.js
server/graph/resolvers/system.js
+6
-7
tree.js
server/graph/resolvers/tree.js
+6
-6
system.graphql
server/graph/schemas/system.graphql
+2
-12
pages.js
server/models/pages.js
+42
-4
ultraviolet-administrative-tools.svg
ux/public/_assets/icons/ultraviolet-administrative-tools.svg
+2
-0
ultraviolet-asciidoc.svg
ux/public/_assets/icons/ultraviolet-asciidoc.svg
+8
-0
App.vue
ux/src/App.vue
+6
-0
FileManager.vue
ux/src/components/FileManager.vue
+145
-47
FolderDeleteDialog.vue
ux/src/components/FolderDeleteDialog.vue
+109
-0
PageNewMenu.vue
ux/src/components/PageNewMenu.vue
+15
-9
PageSaveDialog.vue
ux/src/components/PageSaveDialog.vue
+67
-15
TreeNav.vue
ux/src/components/TreeNav.vue
+19
-1
TreeNode.vue
ux/src/components/TreeNode.vue
+1
-1
en.json
ux/src/i18n/locales/en.json
+15
-8
AdminLayout.vue
ux/src/layouts/AdminLayout.vue
+1
-1
ProfileLayout.vue
ux/src/layouts/ProfileLayout.vue
+15
-13
AdminFlags.vue
ux/src/pages/AdminFlags.vue
+110
-76
AdminScheduler.vue
ux/src/pages/AdminScheduler.vue
+1
-1
flags.js
ux/src/stores/flags.js
+36
-0
No files found.
server/app/data.yml
View file @
b4769b9a
...
...
@@ -71,8 +71,9 @@ defaults:
authJwtRenewablePeriod
:
'
14d'
enforceSameOriginReferrerPolicy
:
true
flags
:
ldapdebug
:
false
sqllog
:
false
experimental
:
false
authDebug
:
false
sqlLog
:
false
# System defaults
channel
:
NEXT
cors
:
...
...
server/core/config.js
View file @
b4769b9a
...
...
@@ -133,7 +133,7 @@ module.exports = {
* Apply Dev Flags
*/
async
applyFlags
()
{
WIKI
.
db
.
knex
.
client
.
config
.
debug
=
WIKI
.
config
.
flags
.
sql
l
og
WIKI
.
db
.
knex
.
client
.
config
.
debug
=
WIKI
.
config
.
flags
.
sql
L
og
},
/**
...
...
server/db/migrations/3.0.0.js
View file @
b4769b9a
...
...
@@ -285,7 +285,7 @@ exports.up = async knex => {
table
.
specificType
(
'folderPath'
,
'ltree'
).
index
().
index
(
'tree_folderpath_gist_index'
,
{
indexType
:
'GIST'
})
table
.
string
(
'fileName'
).
notNullable
().
index
()
table
.
enu
(
'type'
,
[
'folder'
,
'page'
,
'asset'
]).
notNullable
().
index
()
table
.
uuid
(
'targetId
'
).
index
()
table
.
string
(
'localeCode'
,
5
).
notNullable
().
defaultTo
(
'en
'
).
index
()
table
.
string
(
'title'
).
notNullable
()
table
.
jsonb
(
'meta'
).
notNullable
().
defaultTo
(
'{}'
)
table
.
timestamp
(
'createdAt'
).
notNullable
().
defaultTo
(
knex
.
fn
.
now
())
...
...
@@ -372,6 +372,7 @@ exports.up = async knex => {
table
.
string
(
'localeCode'
,
5
).
references
(
'code'
).
inTable
(
'locales'
).
index
()
table
.
uuid
(
'authorId'
).
notNullable
().
references
(
'id'
).
inTable
(
'users'
).
index
()
table
.
uuid
(
'creatorId'
).
notNullable
().
references
(
'id'
).
inTable
(
'users'
).
index
()
table
.
uuid
(
'ownerId'
).
notNullable
().
references
(
'id'
).
inTable
(
'users'
).
index
()
table
.
uuid
(
'siteId'
).
notNullable
().
references
(
'id'
).
inTable
(
'sites'
).
index
()
})
.
table
(
'storage'
,
table
=>
{
...
...
@@ -440,6 +441,14 @@ exports.up = async knex => {
}
},
{
key
:
'flags'
,
value
:
{
experimental
:
false
,
authDebug
:
false
,
sqlLog
:
false
}
},
{
key
:
'icons'
,
value
:
{
fa
:
{
...
...
server/graph/resolvers/system.js
View file @
b4769b9a
...
...
@@ -11,9 +11,7 @@ const graphHelper = require('../../helpers/graph')
module
.
exports
=
{
Query
:
{
systemFlags
()
{
return
_
.
transform
(
WIKI
.
config
.
flags
,
(
result
,
value
,
key
)
=>
{
result
.
push
({
key
,
value
})
},
[])
return
WIKI
.
config
.
flags
},
async
systemInfo
()
{
return
{}
},
async
systemExtensions
()
{
...
...
@@ -150,9 +148,10 @@ module.exports = {
}
},
async
updateSystemFlags
(
obj
,
args
,
context
)
{
WIKI
.
config
.
flags
=
_
.
transform
(
args
.
flags
,
(
result
,
row
)
=>
{
_
.
set
(
result
,
row
.
key
,
row
.
value
)
},
{})
WIKI
.
config
.
flags
=
{
...
WIKI
.
config
.
flags
,
...
args
.
flags
}
await
WIKI
.
configSvc
.
applyFlags
()
await
WIKI
.
configSvc
.
saveToDb
([
'flags'
])
return
{
...
...
@@ -164,7 +163,7 @@ module.exports = {
// TODO: broadcast config update
await
WIKI
.
configSvc
.
saveToDb
([
'security'
])
return
{
status
:
graphHelper
.
generateSuccess
(
'System Security configuration applied successfully'
)
operation
:
graphHelper
.
generateSuccess
(
'System Security configuration applied successfully'
)
}
}
},
...
...
server/graph/resolvers/tree.js
View file @
b4769b9a
...
...
@@ -7,7 +7,7 @@ const typeResolvers = {
asset
:
'TreeItemAsset'
}
const
rePathName
=
/^
[
a-z0-9
_
]
+$/
const
rePathName
=
/^
[
a-z0-9
-
]
+$/
const
reTitle
=
/^
[^
<>"
]
+$/
module
.
exports
=
{
...
...
@@ -41,7 +41,7 @@ module.exports = {
if
(
args
.
parentId
)
{
const
parent
=
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
args
.
parentId
).
first
()
if
(
parent
)
{
parentPath
=
parent
.
folderPath
?
`
${
parent
.
folderPath
}
.
${
parent
.
fileName
}
`
:
parent
.
fileName
parentPath
=
(
parent
.
folderPath
?
`
${
parent
.
folderPath
}
.
${
parent
.
fileName
}
`
:
parent
.
fileName
).
replaceAll
(
'-'
,
'_'
)
}
}
else
if
(
args
.
parentPath
)
{
parentPath
=
args
.
parentPath
.
replaceAll
(
'/'
,
'.'
).
replaceAll
(
'-'
,
'_'
).
toLowerCase
()
...
...
@@ -101,11 +101,11 @@ module.exports = {
if
(
parent
)
{
parentPath
=
parent
.
folderPath
?
`
${
parent
.
folderPath
}
.
${
parent
.
fileName
}
`
:
parent
.
fileName
}
parentPath
=
parentPath
.
replaceAll
(
'-'
,
'_'
)
}
// Validate path name
const
pathName
=
args
.
pathName
.
replaceAll
(
'-'
,
'_'
)
if
(
!
rePathName
.
test
(
pathName
))
{
if
(
!
rePathName
.
test
(
args
.
pathName
))
{
throw
new
Error
(
'ERR_INVALID_PATH_NAME'
)
}
...
...
@@ -118,7 +118,7 @@ module.exports = {
const
existingFolder
=
await
WIKI
.
db
.
knex
(
'tree'
).
where
({
siteId
:
args
.
siteId
,
folderPath
:
parentPath
,
fileName
:
pathName
fileName
:
args
.
pathName
}).
first
()
if
(
existingFolder
)
{
throw
new
Error
(
'ERR_FOLDER_ALREADY_EXISTS'
)
...
...
@@ -127,7 +127,7 @@ module.exports = {
// Create folder
await
WIKI
.
db
.
knex
(
'tree'
).
insert
({
folderPath
:
parentPath
,
fileName
:
pathName
,
fileName
:
args
.
pathName
,
type
:
'folder'
,
title
:
args
.
title
,
siteId
:
args
.
siteId
...
...
server/graph/schemas/system.graphql
View file @
b4769b9a
...
...
@@ -4,7 +4,7 @@
extend
type
Query
{
systemExtensions
:
[
SystemExtension
]
systemFlags
:
[
SystemFlag
]
systemFlags
:
JSON
systemInfo
:
SystemInfo
systemInstances
:
[
SystemInstance
]
systemSecurity
:
SystemSecurity
...
...
@@ -31,7 +31,7 @@ extend type Mutation {
):
DefaultResponse
updateSystemFlags
(
flags
:
[
SystemFlagInput
]
!
flags
:
JSON
!
):
DefaultResponse
updateSystemSecurity
(
...
...
@@ -60,16 +60,6 @@ extend type Mutation {
# TYPES
# -----------------------------------------------
type
SystemFlag
{
key
:
String
value
:
Boolean
}
input
SystemFlagInput
{
key
:
String
!
value
:
Boolean
!
}
type
SystemInfo
{
configFile
:
String
cpuCores
:
Int
...
...
server/models/pages.js
View file @
b4769b9a
...
...
@@ -310,12 +310,12 @@ module.exports = class Page extends Model {
},
contentType
:
WIKI
.
data
.
editors
[
opts
.
editor
]?.
contentType
??
'text'
,
description
:
opts
.
description
,
// dotPath: dotPath,
editor
:
opts
.
editor
,
hash
:
pageHelper
.
generateHash
({
path
:
opts
.
path
,
locale
:
opts
.
locale
}),
icon
:
opts
.
icon
,
isBrowsable
:
opts
.
isBrowsable
??
true
,
localeCode
:
opts
.
locale
,
ownerId
:
opts
.
user
.
id
,
path
:
opts
.
path
,
publishState
:
opts
.
publishState
,
publishEndDate
:
opts
.
publishEndDate
?.
toISO
(),
...
...
@@ -339,6 +339,29 @@ module.exports = class Page extends Model {
// -> Render page to HTML
await
WIKI
.
db
.
pages
.
renderPage
(
page
)
// -> Add to tree
const
pathParts
=
page
.
path
.
split
(
'/'
)
await
WIKI
.
db
.
knex
(
'tree'
).
insert
({
id
:
page
.
id
,
folderPath
:
_
.
initial
(
pathParts
).
join
(
'/'
),
fileName
:
_
.
last
(
pathParts
),
type
:
'page'
,
localeCode
:
page
.
localeCode
,
title
:
page
.
title
,
meta
:
{
authorId
:
page
.
authorId
,
contentType
:
page
.
contentType
,
creatorId
:
page
.
creatorId
,
description
:
page
.
description
,
isBrowsable
:
page
.
isBrowsable
,
ownerId
:
page
.
ownerId
,
publishState
:
page
.
publishState
,
publishEndDate
:
page
.
publishEndDate
,
publishStartDate
:
page
.
publishStartDate
},
siteId
:
page
.
siteId
})
return
page
// TODO: Handle remaining flow
...
...
@@ -590,6 +613,23 @@ module.exports = class Page extends Model {
}
WIKI
.
events
.
outbound
.
emit
(
'deletePageFromCache'
,
page
.
hash
)
// -> Update tree
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
page
.
id
).
update
({
title
:
page
.
title
,
meta
:
{
authorId
:
page
.
authorId
,
contentType
:
page
.
contentType
,
creatorId
:
page
.
creatorId
,
description
:
page
.
description
,
isBrowsable
:
page
.
isBrowsable
,
ownerId
:
page
.
ownerId
,
publishState
:
page
.
publishState
,
publishEndDate
:
page
.
publishEndDate
,
publishStartDate
:
page
.
publishStartDate
},
updatedAt
:
page
.
updatedAt
})
// // -> Update Search Index
// const pageContents = await WIKI.db.pages.query().findById(page.id).select('render')
// page.safeContent = WIKI.db.pages.cleanHTML(pageContents.render)
...
...
@@ -948,12 +988,10 @@ module.exports = class Page extends Model {
// -> Delete page
await
WIKI
.
db
.
pages
.
query
().
delete
().
where
(
'id'
,
page
.
id
)
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
page
.
id
).
del
()
await
WIKI
.
db
.
pages
.
deletePageFromCache
(
page
.
hash
)
WIKI
.
events
.
outbound
.
emit
(
'deletePageFromCache'
,
page
.
hash
)
// -> Rebuild page tree
await
WIKI
.
db
.
pages
.
rebuildTree
()
// -> Delete from Search Index
await
WIKI
.
data
.
searchEngine
.
deleted
(
page
)
...
...
ux/public/_assets/icons/ultraviolet-administrative-tools.svg
0 → 100644
View file @
b4769b9a
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 40 40"
width=
"80px"
height=
"80px"
><path
fill=
"#b6dcfe"
d=
"M17.676,38.5l-1.125-4.501l-0.284-0.076c-2.458-0.658-4.696-1.946-6.473-3.724l-0.208-0.208 l-4.445,1.271l-2.324-4.025l3.335-3.225l-0.076-0.284C5.741,22.475,5.571,21.22,5.571,20s0.17-2.475,0.504-3.729l0.076-0.284 l-3.335-3.225l2.324-4.025l4.445,1.271l0.208-0.208c1.778-1.779,4.016-3.066,6.473-3.724l0.284-0.076L17.676,1.5h4.647 l1.125,4.501l0.284,0.076c2.457,0.657,4.695,1.945,6.473,3.724l0.208,0.208l4.445-1.271l2.324,4.025l-3.335,3.225l0.076,0.284 c0.335,1.253,0.505,2.507,0.505,3.728c0,1.22-0.17,2.475-0.504,3.729l-0.076,0.284l3.335,3.224l-2.324,4.026l-4.445-1.272 l-0.208,0.208c-1.777,1.779-4.016,3.066-6.473,3.724l-0.284,0.076L22.324,38.5H17.676z M20,12.036 c-4.392,0-7.964,3.573-7.964,7.964s3.573,7.964,7.964,7.964s7.964-3.573,7.964-7.964S24.392,12.036,20,12.036z"
/><path
fill=
"#4788c7"
d=
"M21.933,2l0.959,3.837l0.143,0.571l0.569,0.152c2.372,0.635,4.532,1.878,6.248,3.594l0.416,0.417 l0.566-0.162l3.787-1.083l1.934,3.349l-2.843,2.749l-0.423,0.409l0.152,0.568c0.324,1.211,0.488,2.422,0.488,3.599 c0,1.177-0.164,2.388-0.488,3.599l-0.152,0.568l0.423,0.409l2.843,2.749l-1.934,3.349l-3.787-1.083l-0.566-0.162l-0.416,0.417 c-1.715,1.716-3.876,2.959-6.248,3.594l-0.569,0.152l-0.143,0.571L21.934,38h-3.867l-0.959-3.837l-0.143-0.571l-0.569-0.152 c-2.372-0.635-4.533-1.878-6.248-3.594l-0.416-0.417l-0.566,0.162l-3.787,1.083l-1.934-3.349l2.843-2.749l0.423-0.409 l-0.152-0.568C6.235,22.388,6.071,21.177,6.071,20s0.164-2.388,0.488-3.599l0.152-0.568l-0.423-0.409l-2.843-2.749l1.934-3.349 l3.787,1.083l0.566,0.162l0.416-0.417c1.715-1.716,3.876-2.959,6.248-3.594l0.569-0.152l0.143-0.571L18.066,2H21.933 M20,28.464 c4.667,0,8.464-3.797,8.464-8.464c0-4.667-3.797-8.464-8.464-8.464c-4.667,0-8.464,3.797-8.464,8.464 C11.536,24.667,15.333,28.464,20,28.464 M22.714,1h-5.429l-1.149,4.594c-2.569,0.688-4.871,2.027-6.696,3.853L4.903,8.149 l-2.714,4.701l3.405,3.292C5.264,17.375,5.071,18.664,5.071,20s0.192,2.625,0.522,3.857l-3.405,3.292l2.714,4.701l4.538-1.298 c1.825,1.826,4.128,3.165,6.697,3.853L17.286,39h5.429l1.148-4.594c2.569-0.688,4.872-2.027,6.697-3.853l4.538,1.298l2.714-4.701 l-3.405-3.292c0.329-1.232,0.522-2.521,0.522-3.857s-0.192-2.625-0.522-3.857l3.405-3.292l-2.714-4.701l-4.538,1.298 c-1.825-1.826-4.127-3.165-6.696-3.853L22.714,1L22.714,1z M20,27.464c-4.122,0-7.464-3.342-7.464-7.464 c0-4.122,3.342-7.464,7.464-7.464c4.122,0,7.464,3.342,7.464,7.464C27.464,24.122,24.122,27.464,20,27.464L20,27.464z"
/><path
fill=
"#dff0fe"
d=
"M20,9C13.925,9,9,13.925,9,20c0,6.075,4.925,11,11,11s11-4.925,11-11C31,13.925,26.075,9,20,9z M20,24c-2.209,0-4-1.791-4-4c0-2.209,1.791-4,4-4s4,1.791,4,4C24,22.209,22.209,24,20,24z"
/><path
fill=
"#4788c7"
d=
"M20,16c2.209,0,4,1.791,4,4c0,2.209-1.791,4-4,4s-4-1.791-4-4C16,17.791,17.791,16,20,16 M20,15 c-2.757,0-5,2.243-5,5s2.243,5,5,5s5-2.243,5-5S22.757,15,20,15L20,15z"
/><path
fill=
"#fff"
d=
"M21.5 23.5H39.5V39.5H21.5z"
/><path
fill=
"#4788c7"
d=
"M39,24v15H22V24H39 M40,23H21v17h19V23L40,23z"
/><g><path
fill=
"#4788c7"
d=
"M21 23H40V27H21z"
/></g><path
fill=
"none"
stroke=
"#4788c7"
stroke-linecap=
"round"
stroke-miterlimit=
"10"
stroke-width=
"2"
d=
"M27 32L29.5 34.375 34 30"
/></svg>
\ No newline at end of file
ux/public/_assets/icons/ultraviolet-asciidoc.svg
0 → 100644
View file @
b4769b9a
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg
width=
"100%"
height=
"100%"
viewBox=
"0 0 80 80"
version=
"1.1"
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
xml:space=
"preserve"
xmlns:serif=
"http://www.serif.com/"
style=
"fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
>
<path
d=
"M13.687,7.865L34.903,44.827L71.741,44.827C71.99,43.253 72.114,41.595 72.114,39.938C72.114,22.244 57.735,7.865 40,7.865L13.687,7.865Z"
style=
"fill:rgb(152,204,253);fill-rule:nonzero;stroke:rgb(71,136,199);stroke-width:2px;"
/>
<path
d=
"M7.886,19.716L22.306,44.869L7.886,44.869L7.886,19.716Z"
style=
"fill:rgb(152,204,253);fill-rule:nonzero;stroke:rgb(71,136,199);stroke-width:2px;"
/>
<path
d=
"M7.886,55.808L7.886,65.878C7.886,69.317 10.662,72.135 14.143,72.135L37.97,72.135L28.605,55.808C28.605,55.85 7.886,55.85 7.886,55.808Z"
style=
"fill:rgb(152,204,253);fill-rule:nonzero;stroke:rgb(71,136,199);stroke-width:2px;"
/>
<path
d=
"M49.696,70.602C57.487,68.157 63.992,62.77 67.97,55.767L41.16,55.767L49.696,70.602Z"
style=
"fill:rgb(152,204,253);fill-rule:nonzero;stroke:rgb(71,136,199);stroke-width:2px;"
/>
</svg>
ux/src/App.vue
View file @
b4769b9a
...
...
@@ -5,6 +5,7 @@ router-view
<
script
setup
>
import
{
nextTick
,
onMounted
,
reactive
,
watch
}
from
'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useFlagsStore
}
from
'src/stores/flags'
import
{
useSiteStore
}
from
'src/stores/site'
import
{
useUserStore
}
from
'src/stores/user'
import
{
setCssVar
,
useQuasar
}
from
'quasar'
...
...
@@ -17,6 +18,7 @@ const $q = useQuasar()
// STORES
const
flagsStore
=
useFlagsStore
()
const
siteStore
=
useSiteStore
()
const
userStore
=
useUserStore
()
...
...
@@ -67,6 +69,10 @@ if (typeof siteConfig !== 'undefined') {
router
.
beforeEach
(
async
(
to
,
from
)
=>
{
siteStore
.
routerLoading
=
true
// System Flags
if
(
!
flagsStore
.
loaded
)
{
flagsStore
.
load
()
}
// Site Info
if
(
!
siteStore
.
id
)
{
console
.
info
(
'No pre-cached site config. Loading site info...'
)
...
...
ux/src/components/FileManager.vue
View file @
b4769b9a
...
...
@@ -38,6 +38,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
q-drawer.fileman-left(:model-value='true', :width='350')
.q-px-md.q-pb-sm
tree(
ref='treeComp'
:nodes='state.treeNodes'
:roots='state.treeRoots'
v-model:selected='state.currentFolderId'
...
...
@@ -46,7 +47,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
@context-action='treeContextAction'
:display-mode='state.displayMode'
)
q-drawer.fileman-right(:model-value='
true
', :width='350', side='right')
q-drawer.fileman-right(:model-value='
$q.screen.gt.md
', :width='350', side='right')
.q-pa-md
template(v-if='currentFileDetails')
q-img.rounded-borders.q-mb-md(
...
...
@@ -143,7 +144,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
color='grey'
:aria-label='t(`common.actions.refresh`)'
icon='las la-redo-alt'
@click=''
@click='
reloadFolder(state.currentFolderId)
'
)
q-tooltip(anchor='bottom middle', self='top middle')
{{
t
(
`common.actions.refresh`
)
}}
q-separator.q-mr-sm(inset, vertical)
...
...
@@ -172,14 +173,21 @@ q-layout.fileman(view='hHh lpR lFr', container)
icon='las la-cloud-upload-alt'
@click='uploadFile'
)
q-list.fileman-filelist
.fileman-emptylist(v-if='files.length < 1')
template(v-if='state.fileListLoading')
q-spinner.q-mr-sm(color='primary', size='xs', :thickness='3')
span.text-primary Loading...
template(v-else)
q-icon.q-mr-sm(name='las la-exclamation-triangle', size='sm')
span This folder is empty.
q-list.fileman-filelist(v-else)
q-item(
v-for='item of files'
:key='item.id'
clickable
active-class='active'
:active='item.id === state.currentFileId'
@click.native='s
tate.currentFileId = item.id
'
@click.native='s
electItem(item)
'
@dblclick.native='openItem(item)'
)
q-item-section.fileman-filelist-icon(avatar)
...
...
@@ -229,7 +237,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
q-item-section.text-negative Delete
q-footer
q-bar.fileman-path
small.text-caption.text-grey-7
/ foo / bar
small.text-caption.text-grey-7
{{
folderPath
}}
input(
type='file'
...
...
@@ -258,6 +266,7 @@ import { usePageStore } from 'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
import
FolderCreateDialog
from
'src/components/FolderCreateDialog.vue'
import
FolderDeleteDialog
from
'src/components/FolderDeleteDialog.vue'
// QUASAR
...
...
@@ -277,50 +286,34 @@ const { t } = useI18n()
const
state
=
reactive
({
loading
:
0
,
search
:
''
,
currentFolderId
:
''
,
currentFileId
:
''
,
currentFolderId
:
null
,
currentFileId
:
null
,
treeNodes
:
{},
treeRoots
:
[],
displayMode
:
'title'
,
isUploading
:
false
,
shouldCancelUpload
:
false
,
uploadPercentage
:
0
,
fileList
:
[
{
id
:
'1'
,
type
:
'folder'
,
title
:
'Beep Boop'
,
children
:
19
},
{
id
:
'2'
,
type
:
'folder'
,
title
:
'Second Folder'
,
children
:
0
},
{
id
:
'3'
,
type
:
'page'
,
title
:
'Some Page'
,
pageType
:
'markdown'
,
updatedAt
:
'2022-11-24T18:27:00Z'
},
{
id
:
'4'
,
type
:
'file'
,
title
:
'Important Document'
,
fileType
:
'pdf'
,
fileSize
:
19000
}
]
fileList
:
[],
fileListLoading
:
false
})
// REFS
const
fileIpt
=
ref
(
null
)
const
treeComp
=
ref
(
null
)
// COMPUTED
const
folderPath
=
computed
(()
=>
{
if
(
!
state
.
currentFolderId
)
{
return
'/'
}
else
{
const
folderNode
=
state
.
treeNodes
[
state
.
currentFolderId
]
??
{}
return
folderNode
.
folderPath
?
`/
${
folderNode
.
folderPath
}
/
${
folderNode
.
fileName
}
/`
:
`/
${
folderNode
.
fileName
}
/`
}
})
const
files
=
computed
(()
=>
{
return
state
.
fileList
.
map
(
f
=>
{
switch
(
f
.
type
)
{
...
...
@@ -334,7 +327,7 @@ const files = computed(() => {
f
.
caption
=
t
(
`fileman.
${
f
.
pageType
}
PageType`
)
break
}
case
'
file
'
:
{
case
'
asset
'
:
{
f
.
icon
=
fileTypes
[
f
.
fileType
]?.
icon
??
''
f
.
side
=
filesize
(
f
.
fileSize
)
if
(
fileTypes
[
f
.
fileType
])
{
...
...
@@ -382,7 +375,7 @@ const currentFileDetails = computed(() => {
})
break
}
case
'
file
'
:
{
case
'
asset
'
:
{
items
.
push
({
label
:
t
(
'fileman.detailsAssetType'
),
value
:
fileTypes
[
item
.
fileType
]
?
t
(
`fileman.
${
item
.
fileType
}
FileType`
)
:
t
(
'fileman.unknownFileType'
,
{
type
:
item
.
fileType
.
toUpperCase
()
})
...
...
@@ -405,8 +398,8 @@ const currentFileDetails = computed(() => {
// WATCHERS
watch
(()
=>
state
.
currentFolderId
,
(
newValue
)
=>
{
state
.
currentFileId
=
null
watch
(()
=>
state
.
currentFolderId
,
async
(
newValue
)
=>
{
await
loadTree
(
newValue
)
})
// METHODS
...
...
@@ -420,7 +413,16 @@ async function treeLazyLoad (nodeId, { done, fail }) {
done
()
}
async
function
loadTree
(
parentId
,
types
)
{
async
function
loadTree
(
parentId
,
types
,
noCache
=
false
)
{
if
(
!
parentId
)
{
parentId
=
null
state
.
treeRoots
=
[]
}
if
(
parentId
===
state
.
currentFolderId
)
{
state
.
fileListLoading
=
true
state
.
currentFileId
=
null
state
.
fileList
=
[]
}
try
{
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
...
...
@@ -476,15 +478,58 @@ async function loadTree (parentId, types) {
for
(
const
item
of
items
)
{
switch
(
item
.
__typename
)
{
case
'TreeItemFolder'
:
{
state
.
treeNodes
[
item
.
id
]
=
{
text
:
item
.
title
,
fileName
:
item
.
fileName
,
children
:
[]
// -> Tree Nodes
if
(
!
state
.
treeNodes
[
item
.
id
]
||
(
parentId
&&
!
treeComp
.
value
.
isLoaded
(
item
.
id
)))
{
state
.
treeNodes
[
item
.
id
]
=
{
folderPath
:
item
.
folderPath
,
fileName
:
item
.
fileName
,
title
:
item
.
title
,
children
:
[]
}
if
(
item
.
folderPath
)
{
if
(
!
state
.
treeNodes
[
parentId
].
children
.
includes
(
item
.
id
))
{
state
.
treeNodes
[
parentId
].
children
.
push
(
item
.
id
)
}
}
}
// -> Set Tree Roots
if
(
!
item
.
folderPath
)
{
newTreeRoots
.
push
(
item
.
id
)
}
else
{
state
.
treeNodes
[
parentId
].
children
.
push
(
item
.
id
)
}
// -> File List
if
(
parentId
===
state
.
currentFolderId
)
{
state
.
fileList
.
push
({
id
:
item
.
id
,
type
:
'folder'
,
title
:
item
.
title
,
children
:
0
})
}
break
}
case
'TreeItemAsset'
:
{
if
(
parentId
===
state
.
currentFolderId
)
{
state
.
fileList
.
push
({
id
:
item
.
id
,
type
:
'asset'
,
title
:
item
.
title
,
fileType
:
'pdf'
,
fileSize
:
19000
})
}
break
}
case
'TreeItemPage'
:
{
if
(
parentId
===
state
.
currentFolderId
)
{
state
.
fileList
.
push
({
id
:
item
.
id
,
type
:
'page'
,
title
:
item
.
title
,
pageType
:
'markdown'
,
updatedAt
:
'2022-11-24T18:27:00Z'
})
}
break
}
...
...
@@ -501,15 +546,23 @@ async function loadTree (parentId, types) {
caption
:
err
.
message
})
}
if
(
parentId
===
state
.
currentFolderId
)
{
nextTick
(()
=>
{
state
.
fileListLoading
=
false
})
}
}
function
treeContextAction
(
nodeId
,
action
)
{
console
.
info
(
nodeId
,
action
)
switch
(
action
)
{
case
'newFolder'
:
{
newFolder
(
nodeId
)
break
}
case
'del'
:
{
delFolder
(
nodeId
)
break
}
}
}
...
...
@@ -524,6 +577,28 @@ function newFolder (parentId) {
})
}
function
delFolder
(
folderId
)
{
$q
.
dialog
({
component
:
FolderDeleteDialog
,
componentProps
:
{
folderId
,
folderName
:
state
.
treeNodes
[
folderId
].
title
}
}).
onOk
(()
=>
{
for
(
const
nodeId
in
state
.
treeNodes
)
{
if
(
state
.
treeNodes
[
nodeId
].
children
.
includes
(
folderId
))
{
state
.
treeNodes
[
nodeId
].
children
=
state
.
treeNodes
[
nodeId
].
children
.
filter
(
c
=>
c
!==
folderId
)
}
}
delete
state
.
treeNodes
[
folderId
]
})
}
function
reloadFolder
(
folderId
)
{
loadTree
(
folderId
,
null
,
true
)
treeComp
.
value
.
resetLoaded
()
}
// -> Upload Methods
function
uploadFile
()
{
...
...
@@ -603,6 +678,15 @@ function uploadCancel () {
state
.
uploadPercentage
=
0
}
function
selectItem
(
item
)
{
if
(
item
.
type
===
'folder'
)
{
state
.
currentFolderId
=
item
.
id
treeComp
.
value
.
setOpened
(
item
.
id
)
}
else
{
state
.
currentFileId
=
item
.
id
}
}
function
openItem
(
item
)
{
console
.
info
(
item
.
id
)
}
...
...
@@ -662,6 +746,20 @@ onMounted(() => {
}
}
&
-emptylist
{
padding
:
16px
;
font-style
:
italic
;
display
:
flex
;
align-items
:
center
;
@at-root
.body--light
&
{
color
:
$grey-6
;
}
@at-root
.body--dark
&
{
color
:
$dark-4
;
}
}
&
-filelist
{
padding
:
8px
12px
;
...
...
ux/src/components/FolderDeleteDialog.vue
0 → 100644
View file @
b4769b9a
<
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
(
`folderDeleteDialog.title`
)
}}
q-card-section
.text-body2
i18n-t(keypath='folderDeleteDialog.confirm')
template(v-slot:name)
strong
{{
folderName
}}
.text-caption.text-grey.q-mt-sm
{{
t
(
'folderDeleteDialog.folderId'
,
{
id
:
folderId
}
)
}}
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
({
folderId
:
{
type
:
String
,
required
:
true
}
,
folderName
:
{
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 deleteFolder ($id: UUID!) {
deleteFolder(folderId: $id) {
operation {
succeeded
message
}
}
}
`
,
variables
:
{
id
:
props
.
folderId
}
}
)
if
(
resp
?.
data
?.
deleteFolder
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'folderDeleteDialog.deleteSuccess'
)
}
)
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
deleteFolder
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
}
)
}
state
.
isLoading
=
false
}
<
/script
>
ux/src/components/PageNewMenu.vue
View file @
b4769b9a
...
...
@@ -11,15 +11,19 @@ q-menu.translucent-menu(
q-item(clickable, @click='create(`markdown`)')
blueprint-icon(icon='markdown')
q-item-section.q-pr-sm New Markdown Page
q-item(clickable, @click='create(`channel`)')
blueprint-icon(icon='chat')
q-item-section.q-pr-sm New Discussion Space
q-item(clickable, @click='create(`blog`)')
blueprint-icon(icon='typewriter-with-paper')
q-item-section.q-pr-sm New Blog Page
q-item(clickable, @click='create(`api`)')
blueprint-icon(icon='api')
q-item-section.q-pr-sm New API Documentation
q-item(clickable, @click='create(`asciidoc`)')
blueprint-icon(icon='asciidoc')
q-item-section.q-pr-sm New AsciiDoc Page
template(v-if='flagsStore.experimental')
q-item(clickable, @click='create(`channel`)')
blueprint-icon(icon='chat')
q-item-section.q-pr-sm New Discussion Space
q-item(clickable, @click='create(`blog`)')
blueprint-icon(icon='typewriter-with-paper')
q-item-section.q-pr-sm New Blog Page
q-item(clickable, @click='create(`api`)')
blueprint-icon(icon='api')
q-item-section.q-pr-sm New API Documentation
q-item(clickable, @click='create(`redirect`)')
blueprint-icon(icon='advance')
q-item-section.q-pr-sm New Redirection
...
...
@@ -41,6 +45,7 @@ import { useQuasar } from 'quasar'
import
{
usePageStore
}
from
'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
import
{
useFlagsStore
}
from
'src/stores/flags'
// PROPS
...
...
@@ -65,6 +70,7 @@ const $q = useQuasar()
// STORES
const
flagsStore
=
useFlagsStore
()
const
pageStore
=
usePageStore
()
const
siteStore
=
useSiteStore
()
...
...
ux/src/components/PageSaveDialog.vue
View file @
b4769b9a
...
...
@@ -5,17 +5,23 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
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-4
q-scroll-area(
:thumb-style='thumbStyle'
:bar-style='barStyle'
style='height: 300px'
)
.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(
...
...
@@ -31,6 +37,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
q-icon(:name='item.icon', size='sm')
q-item-section
q-item-label
{{
item
.
title
}}
.page-save-dialog-path.font-robotomono
{{
folderPath
}}
q-list.q-py-sm
q-item
blueprint-icon(icon='new-document')
...
...
@@ -197,8 +204,28 @@ const displayModes = [
{
value
:
'path'
,
label
:
t
(
'pageSaveDialog.displayModePath'
)
}
]
const
thumbStyle
=
{
right
:
'1px'
,
borderRadius
:
'5px'
,
backgroundColor
:
'#666'
,
width
:
'5px'
,
opacity
:
0.5
}
const
barStyle
=
{
width
:
'7px'
}
// COMPUTED
const
folderPath
=
computed
(()
=>
{
if
(
!
state
.
currentFolderId
)
{
return
'/'
}
else
{
const
folderNode
=
state
.
treeNodes
[
state
.
currentFolderId
]
??
{}
return
folderNode
.
folderPath
?
`/
${
folderNode
.
folderPath
}
/
${
folderNode
.
fileName
}
/`
:
`/
${
folderNode
.
fileName
}
/`
}
})
const
files
=
computed
(()
=>
{
return
state
.
fileList
.
map
(
f
=>
{
switch
(
f
.
type
)
{
...
...
@@ -274,8 +301,9 @@ async function loadTree (parentId, types) {
switch
(
item
.
__typename
)
{
case
'TreeItemFolder'
:
{
state
.
treeNodes
[
item
.
id
]
=
{
text
:
item
.
title
,
folderPath
:
item
.
folderPath
,
fileName
:
item
.
fileName
,
title
:
item
.
title
,
children
:
[]
}
if
(
!
item
.
folderPath
)
{
...
...
@@ -336,16 +364,23 @@ onMounted(() => {
&
-browser
{
height
:
300px
;
max-height
:
90vh
;
border-bottom
:
1px
solid
$blue-grey-1
;
border-bottom
:
1px
solid
#FFF
;
@at-root
.body--light
&
{
border-bottom-color
:
$blue-grey-1
;
}
@at-root
.body--dark
&
{
border-bottom-color
:
$dark-3
;
}
>
.col-4
{
height
:
300px
;
@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
;
}
}
}
...
...
@@ -372,5 +407,22 @@ onMounted(() => {
}
}
&
-path
{
padding
:
5px
16px
;
font-size
:
12px
;
border-bottom
:
1px
solid
#FFF
;
@at-root
.body--light
&
{
background-color
:
lighten
(
$blue-grey-1
,
4%
);
border-bottom-color
:
$blue-grey-1
;
color
:
$blue-grey-9
;
}
@at-root
.body--dark
&
{
background-color
:
darken
(
$dark-4
,
1%
);
border-bottom-color
:
$dark-1
;
color
:
$blue-grey-3
;
}
}
}
</
style
>
ux/src/components/TreeNav.vue
View file @
b4769b9a
...
...
@@ -95,7 +95,7 @@ const state = reactive({
opened
:
{}
})
// COMP
O
UTED
// COMPUTED
const
selection
=
computed
({
get
()
{
...
...
@@ -120,6 +120,16 @@ function emitContextAction (nodeId, action) {
emit
(
'contextAction'
,
nodeId
,
action
)
}
function
setOpened
(
nodeId
)
{
state
.
opened
[
nodeId
]
=
true
}
function
isLoaded
(
nodeId
)
{
return
state
.
loaded
[
nodeId
]
}
function
resetLoaded
(
nodeId
)
{
state
.
loaded
[
nodeId
]
=
false
}
// PROVIDE
provide
(
'roots'
,
toRef
(
props
,
'roots'
))
...
...
@@ -131,6 +141,14 @@ provide('selection', selection)
provide
(
'emitLazyLoad'
,
emitLazyLoad
)
provide
(
'emitContextAction'
,
emitContextAction
)
// EXPOSE
defineExpose
({
setOpened
,
isLoaded
,
resetLoaded
})
// MOUNTED
onMounted
(()
=>
{
...
...
ux/src/components/TreeNode.vue
View file @
b4769b9a
...
...
@@ -7,7 +7,7 @@ li.treeview-node
size='sm'
@click.stop='hasChildren ? toggleNode() : openNode()'
)
.treeview-label-text
{{
displayMode
===
'path'
?
node
.
fileName
:
node
.
t
ext
}}
.treeview-label-text
{{
displayMode
===
'path'
?
node
.
fileName
:
node
.
t
itle
}}
q-spinner.q-mr-xs(
color='primary'
v-if='state.isLoading'
...
...
ux/src/i18n/locales/en.json
View file @
b4769b9a
...
...
@@ -157,15 +157,13 @@
"admin.extensions.requiresSharp"
:
"Requires Sharp extension"
,
"admin.extensions.subtitle"
:
"Install extensions for extra functionality"
,
"admin.extensions.title"
:
"Extensions"
,
"admin.flags.hidedonatebtn.hint"
:
"You have already donated to this project (thank you!) and want to hide the button from the administration area."
,
"admin.flags.hidedonatebtn.label"
:
"Hide Donate Button"
,
"admin.flags.ldapdebug.hint"
:
"Log detailed debug info on LDAP/AD login attempts."
,
"admin.flags.ldapdebug.label"
:
"LDAP Debug"
,
"admin.flags.sqllog.hint"
:
"Log all queries made to the database to console."
,
"admin.flags.sqllog.label"
:
"SQL Query Logging"
,
"admin.flags.authDebug.hint"
:
"Log detailed debug info of all login / registration attempts."
,
"admin.flags.authDebug.label"
:
"Auth Debug"
,
"admin.flags.sqlLog.hint"
:
"Log all queries made to the database to console."
,
"admin.flags.sqlLog.label"
:
"SQL Query Logging"
,
"admin.flags.subtitle"
:
"Low-level system flags for debugging or experimental purposes"
,
"admin.flags.title"
:
"Flags"
,
"admin.flags.warn.hint"
:
"Doing so may result in data loss
or
broken installation!"
,
"admin.flags.warn.hint"
:
"Doing so may result in data loss
, performance issues or a
broken installation!"
,
"admin.flags.warn.label"
:
"Do NOT enable these flags unless you know what you're doing!"
,
"admin.general.allowComments"
:
"Allow Comments"
,
"admin.general.allowCommentsHint"
:
"Can users leave comments on pages? Can be restricted using Page Rules."
,
...
...
@@ -1603,5 +1601,14 @@
"common.actions.duplicate"
:
"Duplicate"
,
"common.actions.moveTo"
:
"Move To"
,
"pageSaveDialog.displayModeTitle"
:
"Title"
,
"pageSaveDialog.displayModePath"
:
"Path"
"pageSaveDialog.displayModePath"
:
"Path"
,
"folderDeleteDialog.title"
:
"Confirm Delete Folder"
,
"folderDeleteDialog.confirm"
:
"Are you sure you want to delete folder {name} and all its content?"
,
"folderDeleteDialog.folderId"
:
"Folder ID {id}"
,
"folderDeleteDialog.deleteSuccess"
:
"Folder has been deleted successfully."
,
"admin.flags.experimental.label"
:
"Experimental Features"
,
"admin.flags.experimental.hint"
:
"Enable unstable / unfinished features. DO NOT enable in a production environment!"
,
"admin.flags.advanced.label"
:
"Custom Configuration"
,
"admin.flags.advanced.hint"
:
"Set custom configuration flags. Note that all values are public to all users! Do not insert senstive data."
,
"admin.flags.saveSuccess"
:
"Flags have been updated successfully."
}
ux/src/layouts/AdminLayout.vue
View file @
b4769b9a
...
...
@@ -29,7 +29,7 @@ q-layout.admin(view='hHh Lpr lff')
:thumb-style='thumbStyle'
:bar-style='barStyle'
)
q-list.text-white(padding, dense)
q-list.text-white
.q-pb-lg
(padding, dense)
q-item.q-mb-sm
q-item-section
q-btn.acrylic-btn(
...
...
ux/src/layouts/ProfileLayout.vue
View file @
b4769b9a
...
...
@@ -5,19 +5,19 @@ q-layout(view='hHh Lpr lff')
.layout-profile-card
.layout-profile-sd
q-list
q-item(
v-for='navItem of sidenav'
:key='navItem.key
'
clickable
:to='`/_profile/` + navItem.key'
active-class='is-active'
:disabled='navItem.disabled'
v-ripple
)
q-item-section(side)
q-icon(:name='navItem.icon')
q-item-section
q-item-label
{{
navItem
.
label
}}
template(v-for='navItem of sidenav' :key='navItem.key')
q-item(
v-if='!navItem.disabled || flagsStore.experimental
'
clickable
:to='`/_profile/` + navItem.key'
active-class='is-active'
:disabled='navItem.disabled'
v-ripple
)
q-item-section(side)
q-icon(:name='navItem.icon')
q-item-section
q-item-label
{{
navItem
.
label
}}
q-separator.q-my-sm(inset)
q-item(
clickable
...
...
@@ -48,6 +48,7 @@ import { useI18n } from 'vue-i18n'
import
{
useMeta
,
useQuasar
}
from
'quasar'
import
{
onMounted
,
reactive
,
watch
}
from
'vue'
import
{
useFlagsStore
}
from
'src/stores/flags'
import
{
useSiteStore
}
from
'src/stores/site'
import
{
useUserStore
}
from
'src/stores/user'
...
...
@@ -61,6 +62,7 @@ const $q = useQuasar()
// STORES
const
flagsStore
=
useFlagsStore
()
const
siteStore
=
useSiteStore
()
const
userStore
=
useUserStore
()
...
...
ux/src/pages/AdminFlags.vue
View file @
b4769b9a
...
...
@@ -15,13 +15,20 @@ q-page.admin-flags
target='_blank'
type='a'
)
q-btn.q-mr-sm.acrylic-btn(
icon='las la-redo-alt'
flat
color='secondary'
:loading='state.loading > 0'
@click='load'
)
q-btn(
unelevated
icon='fa-solid fa-check'
:label='t(`common.actions.apply`)'
color='secondary'
@click='save'
:loading='
loading
'
:loading='
state.loading > 0
'
)
q-separator(inset)
.row.q-pa-md.q-col-gutter-md
...
...
@@ -39,44 +46,60 @@ q-page.admin-flags
q-item(tag='label')
blueprint-icon(icon='flag-filled')
q-item-section
q-item-label
{{
t
(
`admin.flags.
ldapdebug
.label`
)
}}
q-item-label(caption)
{{
t
(
`admin.flags.
ldapdebug
.hint`
)
}}
q-item-label
{{
t
(
`admin.flags.
experimental
.label`
)
}}
q-item-label(caption)
{{
t
(
`admin.flags.
experimental
.hint`
)
}}
q-item-section(avatar)
q-toggle(
v-model='
flags.ldapdebug
'
color='
primary
'
v-model='
state.flags.experimental
'
color='
negative
'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='t(`admin.flags.
ldapdebug
.label`)'
:aria-label='t(`admin.flags.
experimental
.label`)'
)
q-separator.q-my-sm(inset)
q-item(tag='label')
blueprint-icon(icon='flag-filled')
q-item-section
q-item-label
{{
t
(
`admin.flags.
sqllo
g.label`
)
}}
q-item-label(caption)
{{
t
(
`admin.flags.
sqllo
g.hint`
)
}}
q-item-label
{{
t
(
`admin.flags.
authDebu
g.label`
)
}}
q-item-label(caption)
{{
t
(
`admin.flags.
authDebu
g.hint`
)
}}
q-item-section(avatar)
q-toggle(
v-model='
flags.sqllo
g'
color='
primary
'
v-model='
state.flags.authDebu
g'
color='
negative
'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='t(`admin.flags.
sqllo
g.label`)'
:aria-label='t(`admin.flags.
authDebu
g.label`)'
)
q-card.shadow-1.q-py-sm.q-mt-md
q-separator.q-my-sm(inset)
q-item(tag='label')
blueprint-icon(icon='
heart-outline
')
blueprint-icon(icon='
flag-filled
')
q-item-section
q-item-label
{{
t
(
`admin.flags.
hidedonatebtn
.label`
)
}}
q-item-label(caption)
{{
t
(
`admin.flags.
hidedonatebtn
.hint`
)
}}
q-item-label
{{
t
(
`admin.flags.
sqlLog
.label`
)
}}
q-item-label(caption)
{{
t
(
`admin.flags.
sqlLog
.hint`
)
}}
q-item-section(avatar)
q-toggle(
v-model='
flags.hidedonatebtn
'
color='
primary
'
v-model='
state.flags.sqlLog
'
color='
negative
'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='t(`admin.flags.
hidedonatebtn
.label`)'
:aria-label='t(`admin.flags.
sqlLog
.label`)'
)
q-card.shadow-1.q-py-sm.q-mt-md
q-item
blueprint-icon(icon='administrative-tools')
q-item-section
q-item-label
{{
t
(
`admin.flags.advanced.label`
)
}}
q-item-label(caption)
{{
t
(
`admin.flags.advanced.hint`
)
}}
q-item-section(avatar)
q-btn(
:label='t(`common.actions.edit`)'
unelevated
icon='las la-code'
color='primary'
text-color='white'
@click=''
disabled
)
.col-12.col-lg-5.gt-md
.q-pa-md.text-center
...
...
@@ -85,12 +108,13 @@ q-page.admin-flags
<
script
setup
>
import
gql
from
'graphql-tag'
import
{
defineAsyncComponent
,
onMounted
,
reactive
,
ref
,
watch
}
from
'vue'
import
{
transform
}
from
'lodash-es'
import
{
defineAsyncComponent
,
onMounted
,
reactive
,
ref
}
from
'vue'
import
{
cloneDeep
,
omit
}
from
'lodash-es'
import
{
useMeta
,
useQuasar
}
from
'quasar'
import
{
useI18n
}
from
'vue-i18n'
import
{
useSiteStore
}
from
'src/stores/site'
import
{
useFlagsStore
}
from
'src/stores/flags'
// QUASAR
...
...
@@ -98,6 +122,7 @@ const $q = useQuasar()
// STORES
const
flagsStore
=
useFlagsStore
()
const
siteStore
=
useSiteStore
()
// I18N
...
...
@@ -110,67 +135,76 @@ useMeta({
title
:
t
(
'admin.flags.title'
)
})
const
loading
=
ref
(
false
)
const
flags
=
reactive
({
ldapdebug
:
false
,
sqllog
:
false
,
hidedonatebtn
:
false
// DATA
const
state
=
reactive
({
loading
:
0
,
flags
:
{
experimental
:
false
,
authDebug
:
false
,
sqlLog
:
false
}
})
const
save
=
async
()
=>
{
// METHODS
async
function
load
()
{
state
.
loading
++
$q
.
loading
.
show
()
await
flagsStore
.
load
()
state
.
flags
=
omit
(
cloneDeep
(
flagsStore
.
$state
),
[
'loaded'
])
$q
.
loading
.
hide
()
state
.
loading
--
}
// methods: {
// async save () {
// try {
// await this.$apollo.mutate({
// mutation: gql`
// mutation updateFlags (
// $flags: [SystemFlagInput]!
// ) {
// updateSystemFlags(
// flags: $flags
// ) {
// status {
// succeeded
// slug
// message
// }
// }
// }
// `,
// variables: {
// flags: _transform(this.flags, (result, value, key) => {
// result.push({ key, value })
// }, [])
// },
// watchLoading (isLoading) {
// this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-flags-update')
// }
// })
// this.$store.commit('showNotification', {
// style: 'success',
// message: 'Flags applied successfully.',
// icon: 'check'
// })
// } catch (err) {
// this.$store.commit('pushGraphError', err)
// }
// }
// }
// apollo: {
// flags: {
// query: gql``,
// fetchPolicy: 'network-only',
// update: (data) => _transform(data.system.flags, (result, row) => {
// _set(result, row.key, row.value)
// }, {}),
// watchLoading (isLoading) {
// this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-flags-refresh')
// }
// }
// }
async
function
save
()
{
if
(
state
.
loading
>
0
)
{
return
}
state
.
loading
++
try
{
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation updateFlags (
$flags: JSON!
) {
updateSystemFlags(
flags: $flags
) {
operation {
succeeded
message
}
}
}
`
,
variables
:
{
flags
:
state
.
flags
}
})
if
(
resp
?.
data
?.
updateSystemFlags
?.
operation
?.
succeeded
)
{
load
()
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.flags.saveSuccess'
)
})
}
else
{
throw
new
Error
(
resp
?.
data
?.
updateSystemFlags
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
state
.
loading
--
}
// MOUNTED
onMounted
(
async
()
=>
{
load
()
})
</
script
>
<
style
lang=
'scss'
>
...
...
ux/src/pages/AdminScheduler.vue
View file @
b4769b9a
...
...
@@ -577,7 +577,7 @@ async function cancelJob (jobId) {
}
})
if
(
resp
?.
data
?.
cancelJob
?.
operation
?.
succeeded
)
{
this
.
load
()
load
()
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.scheduler.cancelJobSuccess'
)
...
...
ux/src/stores/flags.js
0 → 100644
View file @
b4769b9a
import
{
defineStore
}
from
'pinia'
import
gql
from
'graphql-tag'
export
const
useFlagsStore
=
defineStore
(
'flags'
,
{
state
:
()
=>
({
loaded
:
false
,
experimental
:
false
}),
getters
:
{},
actions
:
{
async
load
()
{
try
{
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query getFlag {
systemFlags
}
`
,
fetchPolicy
:
'network-only'
})
const
systemFlags
=
resp
.
data
.
systemFlags
if
(
systemFlags
)
{
this
.
$patch
({
...
systemFlags
,
loaded
:
true
})
}
else
{
throw
new
Error
(
'Could not fetch system flags.'
)
}
}
catch
(
err
)
{
console
.
warn
(
err
.
networkError
?.
result
??
err
.
message
)
throw
err
}
}
}
})
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