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
bfbb64a7
Unverified
Commit
bfbb64a7
authored
Dec 30, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: file manager improvements + admin user edit fixes
parent
b4769b9a
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
288 additions
and
91 deletions
+288
-91
tree.js
server/graph/resolvers/tree.js
+49
-0
user.js
server/graph/resolvers/user.js
+6
-2
tree.graphql
server/graph/schemas/tree.graphql
+1
-1
package.json
ux/package.json
+1
-0
fileman-page.svg
ux/public/_assets/illustrations/fileman-page.svg
+13
-0
FileManager.vue
ux/src/components/FileManager.vue
+143
-29
FolderCreateDialog.vue
ux/src/components/FolderCreateDialog.vue
+2
-0
TreeNav.vue
ux/src/components/TreeNav.vue
+6
-2
UserEditOverlay.vue
ux/src/components/UserEditOverlay.vue
+33
-31
en.json
ux/src/i18n/locales/en.json
+2
-1
AdminLayout.vue
ux/src/layouts/AdminLayout.vue
+28
-25
Index.vue
ux/src/pages/Index.vue
+4
-0
yarn.lock
ux/yarn.lock
+0
-0
No files found.
server/graph/resolvers/tree.js
View file @
bfbb64a7
...
...
@@ -91,6 +91,9 @@ module.exports = {
}
},
Mutation
:
{
/**
* CREATE FOLDER
*/
async
createFolder
(
obj
,
args
,
context
)
{
try
{
// Get parent path
...
...
@@ -138,6 +141,52 @@ module.exports = {
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
}
},
/**
* DELETE FOLDER
*/
async
deleteFolder
(
obj
,
args
,
context
)
{
try
{
// Get folder
const
folder
=
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
args
.
folderId
).
first
()
const
folderPath
=
folder
.
folderPath
?
`
${
folder
.
folderPath
}
.
${
folder
.
fileName
}
`
:
folder
.
fileName
WIKI
.
logger
.
debug
(
`Deleting folder
${
folder
.
id
}
at path
${
folderPath
}
...`
)
// Delete all children
const
deletedNodes
=
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'folderPath'
,
'~'
,
`
${
folderPath
}
.*`
).
del
().
returning
([
'id'
,
'type'
])
// Delete folders
const
deletedFolders
=
deletedNodes
.
filter
(
n
=>
n
.
type
===
'folder'
).
map
(
n
=>
n
.
id
)
if
(
deletedFolders
.
length
>
0
)
{
WIKI
.
logger
.
debug
(
`Deleted
${
deletedFolders
.
length
}
children folders.`
)
}
// Delete pages
const
deletedPages
=
deletedNodes
.
filter
(
n
=>
n
.
type
===
'page'
).
map
(
n
=>
n
.
id
)
if
(
deletedPages
.
length
>
0
)
{
WIKI
.
logger
.
debug
(
`Deleting
${
deletedPages
.
length
}
children pages...`
)
// TODO: Delete page
}
// Delete assets
const
deletedAssets
=
deletedNodes
.
filter
(
n
=>
n
.
type
===
'asset'
).
map
(
n
=>
n
.
id
)
if
(
deletedAssets
.
length
>
0
)
{
WIKI
.
logger
.
debug
(
`Deleting
${
deletedPages
.
length
}
children assets...`
)
// TODO: Delete asset
}
// Delete the folder itself
await
WIKI
.
db
.
knex
(
'tree'
).
where
(
'id'
,
folder
.
id
).
del
()
WIKI
.
logger
.
debug
(
`Deleting folder
${
folder
.
id
}
successfully.`
)
return
{
operation
:
graphHelper
.
generateSuccess
(
'Folder deleted successfully'
)
}
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
}
}
},
TreeItem
:
{
...
...
server/graph/resolvers/user.js
View file @
bfbb64a7
...
...
@@ -352,8 +352,12 @@ module.exports = {
strategyKey
:
authStrategy
.
module
,
strategyIcon
:
authModule
.
icon
,
config
:
authStrategy
.
module
===
'local'
?
{
isTfaSetup
:
value
.
tfaSecret
?.
length
>
0
}
:
{}
isPasswordSet
:
value
.
password
?.
length
>
0
,
isTfaSetup
:
value
.
tfaSecret
?.
length
>
0
,
isTfaRequired
:
value
.
tfaRequired
??
false
,
mustChangePwd
:
value
.
mustChangePwd
??
false
,
restrictLogin
:
value
.
restrictLogin
??
false
}
:
value
})
},
[])
},
...
...
server/graph/schemas/tree.graphql
View file @
bfbb64a7
...
...
@@ -76,7 +76,7 @@ type TreeItemPage {
depth
:
Int
fileName
:
String
folderPath
:
String
pageE
ditor
:
String
e
ditor
:
String
pageType
:
String
title
:
String
updatedAt
:
Date
...
...
ux/package.json
View file @
bfbb64a7
...
...
@@ -63,6 +63,7 @@
"codemirror"
:
"6.0.1"
,
"filesize"
:
"10.0.5"
,
"filesize-parser"
:
"1.5.0"
,
"fuse.js"
:
"6.6.2"
,
"graphql"
:
"16.6.0"
,
"graphql-tag"
:
"2.12.6"
,
"js-cookie"
:
"3.0.1"
,
...
...
ux/public/_assets/illustrations/fileman-page.svg
0 → 100644
View file @
bfbb64a7
<?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 320 200"
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;"
>
<rect
x=
"0"
y=
"0"
width=
"320"
height=
"200"
style=
"fill:url(#_Linear1);"
/>
<path
d=
"M203.194,73.306L175.694,45.806C175.18,45.289 174.481,45 173.75,45L129.75,45C122.168,45 116,51.168 116,58.75L116,141.25C116,148.832 122.168,155 129.75,155L190.25,155C197.832,155 204,148.832 204,141.25L204,75.25C204,74.521 203.711,73.82 203.194,73.306Z"
style=
"fill:rgb(107,227,162);fill-rule:nonzero;"
/>
<path
d=
"M204,78L184.75,78C177.168,78 171,71.832 171,64.25L171,45C171,43.482 172.229,42.25 173.75,42.25C175.271,42.25 176.5,43.482 176.5,45L176.5,64.25C176.5,68.798 180.202,72.5 184.75,72.5L204,72.5C205.521,72.5 206.75,73.732 206.75,75.25C206.75,76.768 205.521,78 204,78Z"
style=
"fill:rgb(50,69,97);fill-rule:nonzero;"
/>
<path
d=
"M182,102.75L138,102.75C136.479,102.75 135.25,101.518 135.25,100C135.25,98.482 136.479,97.25 138,97.25L182,97.25C183.521,97.25 184.75,98.482 184.75,100C184.75,101.518 183.521,102.75 182,102.75Z"
style=
"fill:rgb(50,69,97);fill-rule:nonzero;"
/>
<path
d=
"M182,116.5L138,116.5C136.479,116.5 135.25,115.268 135.25,113.75C135.25,112.232 136.479,111 138,111L182,111C183.521,111 184.75,112.232 184.75,113.75C184.75,115.268 183.521,116.5 182,116.5Z"
style=
"fill:rgb(50,69,97);fill-rule:nonzero;"
/>
<path
d=
"M165.5,130.25L138,130.25C136.479,130.25 135.25,129.018 135.25,127.5C135.25,125.982 136.479,124.75 138,124.75L165.5,124.75C167.021,124.75 168.25,125.982 168.25,127.5C168.25,129.018 167.021,130.25 165.5,130.25Z"
style=
"fill:rgb(50,69,97);fill-rule:nonzero;"
/>
<defs>
<linearGradient
id=
"_Linear1"
x1=
"0"
y1=
"0"
x2=
"1"
y2=
"0"
gradientUnits=
"userSpaceOnUse"
gradientTransform=
"matrix(320,100,-62.5,200,0,100)"
><stop
offset=
"0"
style=
"stop-color:rgb(38,43,49);stop-opacity:1"
/><stop
offset=
"1"
style=
"stop-color:rgb(22,27,33);stop-opacity:1"
/></linearGradient>
</defs>
</svg>
ux/src/components/FileManager.vue
View file @
bfbb64a7
...
...
@@ -13,6 +13,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
ref='searchField'
style='width: 100%;'
label='Search folder...'
:debounce='500'
)
template(v-slot:prepend)
q-icon(name='las la-search')
...
...
@@ -51,7 +52,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
.q-pa-md
template(v-if='currentFileDetails')
q-img.rounded-borders.q-mb-md(
src='
https://picsum.photos/id/134/340/340
'
src='
/_assets/illustrations/fileman-page.svg
'
width='100%'
fit='cover'
:ratio='16/10'
...
...
@@ -129,13 +130,21 @@ q-layout.fileman(view='hHh lpR lFr', container)
size='xs'
)
q-item-section.q-pr-sm Browse Using Titles
q-item(clickable)
q-item(clickable
, @click='state.isCompact = !state.isCompact'
)
q-item-section(side)
q-icon(name='las la-stop', color='grey', size='xs')
q-icon(
:name='state.isCompact ? `las la-check-square` : `las la-stop`'
:color='state.isCompact ? `positive` : `grey`'
size='xs'
)
q-item-section.q-pr-sm Compact List
q-item(clickable)
q-item(clickable
, @click='state.shouldShowFolders = !state.shouldShowFolders'
)
q-item-section(side)
q-icon(name='las la-check-square', color='positive', size='xs')
q-icon(
:name='state.shouldShowFolders ? `las la-check-square` : `las la-stop`'
:color='state.shouldShowFolders ? `positive` : `grey`'
size='xs'
)
q-item-section.q-pr-sm Show Folders
q-btn.q-mr-sm(
flat
...
...
@@ -191,10 +200,10 @@ q-layout.fileman(view='hHh lpR lFr', container)
@dblclick.native='openItem(item)'
)
q-item-section.fileman-filelist-icon(avatar)
q-icon(:name='item.icon',
size='xl
')
q-icon(:name='item.icon',
:size='state.isCompact ? `md` : `xl`
')
q-item-section.fileman-filelist-label
q-item-label
{{
item
.
title
}}
q-item-label(caption)
{{
item
.
caption
}}
q-item-label(caption
, v-if='!state.isCompact'
)
{{
item
.
caption
}}
q-item-section.fileman-filelist-side(side, v-if='item.side')
.text-caption
{{
item
.
side
}}
//- RIGHT-CLICK MENU
...
...
@@ -211,11 +220,11 @@ q-layout.fileman(view='hHh lpR lFr', container)
q-item-section(side)
q-icon(name='las la-edit', color='orange')
q-item-section Edit
q-item(clickable, v-if='item.type !== `folder`')
q-item(clickable, v-if='item.type !== `folder`'
, @click='openItem(item)'
)
q-item-section(side)
q-icon(name='las la-eye', color='primary')
q-item-section View
q-item(clickable, v-if='item.type !== `folder`')
q-item(clickable, v-if='item.type !== `folder`'
, @click='copyItemURL(item)'
)
q-item-section(side)
q-icon(name='las la-clipboard', color='primary')
q-item-section Copy URL
...
...
@@ -231,7 +240,7 @@ q-layout.fileman(view='hHh lpR lFr', container)
q-item-section(side)
q-icon(name='las la-arrow-right', color='teal')
q-item-section Move to...
q-item(clickable)
q-item(clickable
, @click='delItem(item)'
)
q-item-section(side)
q-icon(name='las la-trash-alt', color='negative')
q-item-section.text-negative Delete
...
...
@@ -255,7 +264,9 @@ import { filesize } from 'filesize'
import
{
useQuasar
}
from
'quasar'
import
{
DateTime
}
from
'luxon'
import
{
cloneDeep
,
find
}
from
'lodash-es'
import
{
useRoute
,
useRouter
}
from
'vue-router'
import
gql
from
'graphql-tag'
import
Fuse
from
'fuse.js/dist/fuse.basic.esm'
import
NewMenu
from
'./PageNewMenu.vue'
import
Tree
from
'./TreeNav.vue'
...
...
@@ -277,6 +288,11 @@ const $q = useQuasar()
const
pageStore
=
usePageStore
()
const
siteStore
=
useSiteStore
()
// ROUTER
const
router
=
useRouter
()
const
route
=
useRoute
()
// I18N
const
{
t
}
=
useI18n
()
...
...
@@ -291,6 +307,8 @@ const state = reactive({
treeNodes
:
{},
treeRoots
:
[],
displayMode
:
'title'
,
isCompact
:
false
,
shouldShowFolders
:
true
,
isUploading
:
false
,
shouldCancelUpload
:
false
,
uploadPercentage
:
0
,
...
...
@@ -314,8 +332,29 @@ const folderPath = computed(() => {
}
})
const
filteredFiles
=
computed
(()
=>
{
if
(
state
.
search
)
{
const
fuse
=
new
Fuse
(
state
.
fileList
,
{
keys
:
[
'title'
,
'fileName'
]
})
return
fuse
.
search
(
state
.
search
).
map
(
n
=>
n
.
item
)
}
else
{
return
state
.
fileList
}
})
const
files
=
computed
(()
=>
{
return
state
.
fileList
.
map
(
f
=>
{
return
filteredFiles
.
value
.
filter
(
f
=>
{
console
.
info
(
f
)
// -> Show Folders Filter
if
(
f
.
type
===
'folder'
&&
!
state
.
shouldShowFolders
)
{
return
false
}
return
true
}).
map
(
f
=>
{
switch
(
f
.
type
)
{
case
'folder'
:
{
f
.
icon
=
fileTypes
.
folder
.
icon
...
...
@@ -413,10 +452,9 @@ async function treeLazyLoad (nodeId, { done, fail }) {
done
()
}
async
function
loadTree
(
parentId
,
types
,
noCache
=
false
)
{
async
function
loadTree
(
parentId
,
types
)
{
if
(
!
parentId
)
{
parentId
=
null
state
.
treeRoots
=
[]
}
if
(
parentId
===
state
.
currentFolderId
)
{
state
.
fileListLoading
=
true
...
...
@@ -451,7 +489,7 @@ async function loadTree (parentId, types, noCache = false) {
title
createdAt
updatedAt
pageE
ditor
e
ditor
}
... on TreeItemAsset {
id
...
...
@@ -479,22 +517,21 @@ async function loadTree (parentId, types, noCache = false) {
switch
(
item
.
__typename
)
{
case
'TreeItemFolder'
:
{
// -> Tree Nodes
if
(
!
state
.
treeNodes
[
item
.
id
]
||
(
parentId
&&
!
treeComp
.
value
.
isLoaded
(
item
.
id
))
)
{
if
(
!
state
.
treeNodes
[
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
)
}
children
:
state
.
treeNodes
[
item
.
id
]?.
children
??
[]
}
}
// -> Set Tree Roots
if
(
!
item
.
folderPath
)
{
// -> Set Ancestors / Tree Roots
if
(
item
.
folderPath
)
{
if
(
!
state
.
treeNodes
[
parentId
].
children
.
includes
(
item
.
id
))
{
state
.
treeNodes
[
parentId
].
children
.
push
(
item
.
id
)
}
}
else
{
newTreeRoots
.
push
(
item
.
id
)
}
...
...
@@ -504,6 +541,7 @@ async function loadTree (parentId, types, noCache = false) {
id
:
item
.
id
,
type
:
'folder'
,
title
:
item
.
title
,
fileName
:
item
.
fileName
,
children
:
0
})
}
...
...
@@ -516,7 +554,9 @@ async function loadTree (parentId, types, noCache = false) {
type
:
'asset'
,
title
:
item
.
title
,
fileType
:
'pdf'
,
fileSize
:
19000
fileSize
:
19000
,
folderPath
:
item
.
folderPath
,
fileName
:
item
.
fileName
})
}
break
...
...
@@ -528,7 +568,9 @@ async function loadTree (parentId, types, noCache = false) {
type
:
'page'
,
title
:
item
.
title
,
pageType
:
'markdown'
,
updatedAt
:
'2022-11-24T18:27:00Z'
updatedAt
:
'2022-11-24T18:27:00Z'
,
folderPath
:
item
.
folderPath
,
fileName
:
item
.
fileName
})
}
break
...
...
@@ -551,6 +593,9 @@ async function loadTree (parentId, types, noCache = false) {
state
.
fileListLoading
=
false
})
}
if
(
parentId
)
{
treeComp
.
value
.
setLoaded
(
parentId
)
}
}
function
treeContextAction
(
nodeId
,
action
)
{
...
...
@@ -566,6 +611,10 @@ function treeContextAction (nodeId, action) {
}
}
// --------------------------------------
// FOLDER METHODS
// --------------------------------------
function
newFolder
(
parentId
)
{
$q
.
dialog
({
component
:
FolderCreateDialog
,
...
...
@@ -577,7 +626,7 @@ function newFolder (parentId) {
})
}
function
delFolder
(
folderId
)
{
function
delFolder
(
folderId
,
mustReload
=
false
)
{
$q
.
dialog
({
component
:
FolderDeleteDialog
,
componentProps
:
{
...
...
@@ -591,15 +640,23 @@ function delFolder (folderId) {
}
}
delete
state
.
treeNodes
[
folderId
]
if
(
state
.
treeRoots
.
includes
(
folderId
))
{
state
.
treeRoots
=
state
.
treeRoots
.
filter
(
n
=>
n
!==
folderId
)
}
if
(
mustReload
)
{
loadTree
(
state
.
currentFolderId
,
null
)
}
})
}
function
reloadFolder
(
folderId
)
{
loadTree
(
folderId
,
null
,
true
)
loadTree
(
folderId
,
null
)
treeComp
.
value
.
resetLoaded
()
}
// -> Upload Methods
// --------------------------------------
// UPLOAD METHODS
// --------------------------------------
function
uploadFile
()
{
fileIpt
.
value
.
click
()
...
...
@@ -678,6 +735,10 @@ function uploadCancel () {
state
.
uploadPercentage
=
0
}
// --------------------------------------
// ITEM LIST ACTIONS
// --------------------------------------
function
selectItem
(
item
)
{
if
(
item
.
type
===
'folder'
)
{
state
.
currentFolderId
=
item
.
id
...
...
@@ -688,7 +749,60 @@ function selectItem (item) {
}
function
openItem
(
item
)
{
console
.
info
(
item
.
id
)
switch
(
item
.
type
)
{
case
'folder'
:
{
return
}
case
'page'
:
{
const
pagePath
=
item
.
folderPath
?
`
${
item
.
folderPath
}
/
${
item
.
fileName
}
`
:
item
.
fileName
router
.
push
(
`/
${
pagePath
}
`
)
close
()
break
}
case
'asset'
:
{
// TODO: Open asset
close
()
break
}
}
}
async
function
copyItemURL
(
item
)
{
try
{
switch
(
item
.
type
)
{
case
'page'
:
{
const
pagePath
=
item
.
folderPath
?
`
${
item
.
folderPath
}
/
${
item
.
fileName
}
`
:
item
.
fileName
await
navigator
.
clipboard
.
writeText
(
`
${
window
.
location
.
origin
}
/
${
pagePath
}
`
)
break
}
case
'asset'
:
{
// TODO: Copy asset URL to clibpard
break
}
default
:
{
throw
new
Error
(
'Invalid Item Type'
)
}
}
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'fileman.copyURLSuccess'
)
})
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
'Failed to copy URL to clipboard.'
,
caption
:
err
.
message
})
}
}
function
delItem
(
item
)
{
switch
(
item
.
type
)
{
case
'folder'
:
{
delFolder
(
item
.
id
,
true
)
break
}
}
}
// MOUNTED
...
...
ux/src/components/FolderCreateDialog.vue
View file @
bfbb64a7
...
...
@@ -19,6 +19,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
lazy-rules='ondemand'
autofocus
ref='iptTitle'
@keyup.enter='create'
)
q-item
blueprint-icon.self-start(icon='file-submodule')
...
...
@@ -34,6 +35,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
:hint='t(`fileman.folderFileNameHint`)'
lazy-rules='ondemand'
@focus='state.pathDirty = true'
@keyup.enter='create'
)
q-card-actions.card-actions
q-space
...
...
ux/src/components/TreeNav.vue
View file @
bfbb64a7
...
...
@@ -126,8 +126,11 @@ function setOpened (nodeId) {
function
isLoaded
(
nodeId
)
{
return
state
.
loaded
[
nodeId
]
}
function
resetLoaded
(
nodeId
)
{
state
.
loaded
[
nodeId
]
=
false
function
setLoaded
(
nodeId
,
value
)
{
state
.
loaded
[
nodeId
]
=
value
}
function
resetLoaded
()
{
state
.
loaded
=
{}
}
// PROVIDE
...
...
@@ -146,6 +149,7 @@ provide('emitContextAction', emitContextAction)
defineExpose
({
setOpened
,
isLoaded
,
setLoaded
,
resetLoaded
})
...
...
ux/src/components/UserEditOverlay.vue
View file @
bfbb64a7
...
...
@@ -38,17 +38,20 @@ q-layout(view='hHh lpR fFf', container)
)
q-drawer.bg-dark-6(:model-value='true', :width='250', dark)
q-list(padding, v-if='state.loading < 1')
q-item
(
template
(
v-for='sc of sections'
:key='`section-` + sc.key'
clickable
:to='{ params: { section: sc.key } }'
active-class='bg-primary text-white'
:disabled='sc.disabled'
)
q-item-section(side)
q-icon(:name='sc.icon', color='white')
q-item-section
{{
sc
.
text
}}
q-item(
v-if='!sc.disabled || flagsStore.experimental'
clickable
:to='{ params: { section: sc.key } }'
active-class='bg-primary text-white'
:disabled='sc.disabled'
)
q-item-section(side)
q-icon(:name='sc.icon', color='white')
q-item-section
{{
sc
.
text
}}
q-page-container
q-page(v-if='state.loading > 0')
.flex.q-pa-lg.items-center
...
...
@@ -268,7 +271,7 @@ q-layout(view='hHh lpR fFf', container)
q-item-section
q-item-label
{{
t
(
`admin.users.changePassword`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.changePasswordHint`
)
}}
q-item-label(caption): strong(:class='localAuth.
password ? `text-positive` : `text-negative`')
{{
localAuth
.
password
?
t
(
`admin.users.pwdSet`
)
:
t
(
`admin.users.pwdNotSet`
)
}}
q-item-label(caption): strong(:class='localAuth.
isPasswordSet ? `text-positive` : `text-negative`')
{{
localAuth
.
isPasswordSet
?
t
(
`admin.users.pwdSet`
)
:
t
(
`admin.users.pwdNotSet`
)
}}
q-item-section(side)
q-btn.acrylic-btn(
flat
...
...
@@ -316,7 +319,7 @@ q-layout(view='hHh lpR fFf', container)
q-item-label(caption)
{{
t
(
`admin.users.tfaRequiredHint`
)
}}
q-item-section(avatar)
q-toggle(
v-model='localAuth.
t
faRequired'
v-model='localAuth.
isT
faRequired'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
...
...
@@ -328,7 +331,7 @@ q-layout(view='hHh lpR fFf', container)
q-item-section
q-item-label
{{
t
(
`admin.users.tfaInvalidate`
)
}}
q-item-label(caption)
{{
t
(
`admin.users.tfaInvalidateHint`
)
}}
q-item-label(caption): strong(:class='localAuth.
tfaSecret ? `text-positive` : `text-negative`')
{{
localAuth
.
tfaSecret
?
t
(
`admin.users.tfaSet`
)
:
t
(
`admin.users.tfaNotSet`
)
}}
q-item-label(caption): strong(:class='localAuth.
isTfaSetup ? `text-positive` : `text-negative`')
{{
localAuth
.
isTfaSetup
?
t
(
`admin.users.tfaSet`
)
:
t
(
`admin.users.tfaNotSet`
)
}}
q-item-section(side)
q-btn.acrylic-btn(
flat
...
...
@@ -348,14 +351,14 @@ q-layout(view='hHh lpR fFf', container)
)
{{
t
(
'admin.users.noLinkedProviders'
)
}}
template(
v-for='(prv, idx) in linkedAuthProviders'
:key='prv.
_i
d'
:key='prv.
authI
d'
)
q-separator.q-my-sm(inset, v-if='idx > 0')
q-item
blueprint-icon(
icon='google
', :hue-rotate='-45')
blueprint-icon(
:icon='prv.strategyIcon
', :hue-rotate='-45')
q-item-section
q-item-label
{{
prv
.
_module
Name
}}
q-item-label(caption)
{{
prv
.
key
}}
q-item-label
{{
prv
.
auth
Name
}}
q-item-label(caption)
{{
prv
.
config
.
key
}}
q-page(v-else-if='route.params.section === `groups`')
.q-pa-md
...
...
@@ -506,7 +509,7 @@ q-layout(view='hHh lpR fFf', container)
<
script
setup
>
import
gql
from
'graphql-tag'
import
{
cloneDeep
,
find
,
findKey
,
map
,
some
}
from
'lodash-es'
import
{
cloneDeep
,
find
,
map
,
some
}
from
'lodash-es'
import
{
DateTime
}
from
'luxon'
import
{
useI18n
}
from
'vue-i18n'
...
...
@@ -515,6 +518,7 @@ import { computed, onMounted, reactive, watch } from 'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useAdminStore
}
from
'src/stores/admin'
import
{
useFlagsStore
}
from
'src/stores/flags'
import
UserChangePwdDialog
from
'./UserChangePwdDialog.vue'
import
UtilCodeEditor
from
'./UtilCodeEditor.vue'
...
...
@@ -526,6 +530,7 @@ const $q = useQuasar()
// STORES
const
adminStore
=
useAdminStore
()
const
flagsStore
=
useFlagsStore
()
// ROUTER
...
...
@@ -553,7 +558,7 @@ const state = reactive({
const
sections
=
[
{
key
:
'overview'
,
text
:
t
(
'admin.users.overview'
),
icon
:
'las la-user'
},
{
key
:
'activity'
,
text
:
t
(
'admin.users.activity'
),
icon
:
'las la-chart-area'
},
{
key
:
'activity'
,
text
:
t
(
'admin.users.activity'
),
icon
:
'las la-chart-area'
,
disabled
:
true
},
{
key
:
'auth'
,
text
:
t
(
'admin.users.auth'
),
icon
:
'las la-key'
},
{
key
:
'groups'
,
text
:
t
(
'admin.users.groups'
),
icon
:
'las la-users'
},
{
key
:
'metadata'
,
text
:
t
(
'admin.users.metadata'
),
icon
:
'las la-clipboard-list'
},
...
...
@@ -576,17 +581,13 @@ const metadata = computed({
}
})
const
localAuthId
=
computed
(()
=>
{
return
findKey
(
state
.
user
.
auth
,
[
'module'
,
'local'
])
})
const
localAuth
=
computed
({
get
()
{
return
localAuthId
.
value
?
state
.
user
.
auth
?.[
localAuthId
.
value
]
||
{}
:
{}
return
find
(
state
.
user
?.
auth
,
[
'strategyKey'
,
'local'
])?.
config
??
{}
},
set
(
val
)
{
if
(
localAuth
Id
.
value
)
{
state
.
user
.
auth
[
localAuthId
.
value
]
=
val
if
(
localAuth
.
value
.
authId
)
{
find
(
state
.
user
.
auth
,
[
'strategyKey'
,
'local'
]).
config
=
val
}
}
})
...
...
@@ -594,12 +595,7 @@ const localAuth = computed({
const
linkedAuthProviders
=
computed
(()
=>
{
if
(
!
state
.
user
?.
auth
)
{
return
[]
}
return
map
(
state
.
user
.
auth
,
(
obj
,
key
)
=>
{
return
{
...
obj
,
_id
:
key
}
}).
filter
(
prv
=>
prv
.
module
!==
'local'
)
return
state
.
user
.
auth
.
filter
(
prv
=>
prv
.
strategyKey
!==
'local'
)
})
// WATCHERS
...
...
@@ -630,7 +626,13 @@ async function fetchUser () {
isSystem
isVerified
isActive
auth
auth {
authId
authName
strategyKey
strategyIcon
config
}
meta
prefs
lastLoginAt
...
...
ux/src/i18n/locales/en.json
View file @
bfbb64a7
...
...
@@ -1610,5 +1610,6 @@
"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."
"admin.flags.saveSuccess"
:
"Flags have been updated successfully."
,
"fileman.copyURLSuccess"
:
"URL has been copied to the clipboard."
}
ux/src/layouts/AdminLayout.vue
View file @
bfbb64a7
...
...
@@ -70,26 +70,27 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-web.svg')
q-item-section
{{
t
(
'admin.general.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/analytics`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-bar-chart.svg')
q-item-section
{{
t
(
'admin.analytics.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/approvals`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-inspection.svg')
q-item-section
{{
t
(
'admin.approval.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/comments`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-comments.svg')
q-item-section
{{
t
(
'admin.comments.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/blocks`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rfid-tag.svg')
q-item-section
{{
t
(
'admin.blocks.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-cashbook.svg')
q-item-section
{{
t
(
'admin.editors.title'
)
}}
template(v-if='flagsStore.experimental')
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/analytics`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-bar-chart.svg')
q-item-section
{{
t
(
'admin.analytics.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/approvals`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-inspection.svg')
q-item-section
{{
t
(
'admin.approval.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/comments`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-comments.svg')
q-item-section
{{
t
(
'admin.comments.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/blocks`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rfid-tag.svg')
q-item-section
{{
t
(
'admin.blocks.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/editors`', v-ripple, active-class='bg-primary text-white', disabled)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-cashbook.svg')
q-item-section
{{
t
(
'admin.editors.title'
)
}}
q-item(:to='`/_admin/` + adminStore.currentSiteId + `/locale`', v-ripple, active-class='bg-primary text-white')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-language.svg')
...
...
@@ -134,7 +135,7 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section
{{
t
(
'admin.api.title'
)
}}
q-item-section(side)
status-light(:color='adminStore.info.isApiEnabled ? `positive` : `negative`')
q-item(to='/_admin/audit', v-ripple, active-class='bg-primary text-white', disabled)
q-item(to='/_admin/audit', v-ripple, active-class='bg-primary text-white', disabled
, v-if='flagsStore.experimental'
)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-event-log.svg')
q-item-section
{{
t
(
'admin.audit.title'
)
}}
...
...
@@ -156,7 +157,7 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section
{{
t
(
'admin.mail.title'
)
}}
q-item-section(side)
status-light(:color='adminStore.info.isMailConfigured ? `positive` : `warning`')
q-item(to='/_admin/rendering', v-ripple, active-class='bg-primary text-white', disabled)
q-item(to='/_admin/rendering', v-ripple, active-class='bg-primary text-white', disabled
, v-if='flagsStore.experimental'
)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-rich-text-converter.svg')
q-item-section
{{
t
(
'admin.rendering.title'
)
}}
...
...
@@ -170,7 +171,7 @@ q-layout.admin(view='hHh Lpr lff')
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-protect.svg')
q-item-section
{{
t
(
'admin.security.title'
)
}}
q-item(to='/_admin/ssl', v-ripple, active-class='bg-primary text-white', disabled)
q-item(to='/_admin/ssl', v-ripple, active-class='bg-primary text-white', disabled
, v-if='flagsStore.experimental'
)
q-item-section(avatar)
q-icon(name='img:/_assets/icons/fluent-security-ssl.svg')
q-item-section
{{
t
(
'admin.ssl.title'
)
}}
...
...
@@ -218,8 +219,9 @@ import { defineAsyncComponent, onMounted, reactive, ref, watch } from 'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useI18n
}
from
'vue-i18n'
import
{
useAdminStore
}
from
'../stores/admin'
import
{
useSiteStore
}
from
'../stores/site'
import
{
useAdminStore
}
from
'src/stores/admin'
import
{
useFlagsStore
}
from
'src/stores/flags'
import
{
useSiteStore
}
from
'src/stores/site'
// COMPONENTS
...
...
@@ -237,6 +239,7 @@ const $q = useQuasar()
// STORES
const
adminStore
=
useAdminStore
()
const
flagsStore
=
useFlagsStore
()
const
siteStore
=
useSiteStore
()
// ROUTER
...
...
ux/src/pages/Index.vue
View file @
bfbb64a7
...
...
@@ -234,6 +234,7 @@ q-page.column
aria-label='Page Data'
@click='togglePageData'
disable
v-if='flagsStore.experimental'
)
q-tooltip(anchor='center left' self='center right') Page Data
q-separator.q-my-sm(inset)
...
...
@@ -322,6 +323,7 @@ import { useI18n } from 'vue-i18n'
import
{
DateTime
}
from
'luxon'
import
{
useEditorStore
}
from
'src/stores/editor'
import
{
useFlagsStore
}
from
'src/stores/flags'
import
{
usePageStore
}
from
'src/stores/page'
import
{
useSiteStore
}
from
'src/stores/site'
...
...
@@ -349,6 +351,7 @@ const $q = useQuasar()
// STORES
const
editorStore
=
useEditorStore
()
const
flagsStore
=
useFlagsStore
()
const
pageStore
=
usePageStore
()
const
siteStore
=
useSiteStore
()
...
...
@@ -702,6 +705,7 @@ async function saveChanges () {
width
:
40px
;
border-radius
:
4px
!
important
;
background-color
:
rgba
(
0
,
0
,
0
,.
75
);
backdrop-filter
:
blur
(
5px
);
color
:
#FFF
;
position
:
fixed
;
right
:
486px
;
...
...
ux/yarn.lock
View file @
bfbb64a7
This diff was suppressed by a .gitattributes entry.
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