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
6e303ac6
Unverified
Commit
6e303ac6
authored
May 28, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(admin): migrate groups dialogs to vue 3 composable
parent
7e344fc6
Show whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
509 additions
and
428 deletions
+509
-428
data.yml
server/app/data.yml
+4
-3
group.js
server/graph/resolvers/group.js
+23
-20
jsconfig.json
ux/jsconfig.json
+1
-0
GroupCreateDialog.vue
ux/src/components/GroupCreateDialog.vue
+61
-47
GroupDeleteDialog.vue
ux/src/components/GroupDeleteDialog.vue
+42
-39
GroupEditOverlay.vue
ux/src/components/GroupEditOverlay.vue
+352
-301
SiteCreateDialog.vue
ux/src/components/SiteCreateDialog.vue
+13
-8
SiteDeleteDialog.vue
ux/src/components/SiteDeleteDialog.vue
+3
-3
en.json
ux/src/i18n/locales/en.json
+2
-1
AdminLayout.vue
ux/src/layouts/AdminLayout.vue
+5
-3
AdminGroups.vue
ux/src/pages/AdminGroups.vue
+2
-2
AdminStorage.vue
ux/src/pages/AdminStorage.vue
+1
-1
No files found.
server/app/data.yml
View file @
6e303ac6
...
...
@@ -131,9 +131,9 @@ groups:
-
'
read:assets'
-
'
read:comments'
-
'
write:comments'
default
Page
Rules
:
-
id
:
default
deny
:
false
defaultRules
:
-
name
:
Default Rule
mode
:
ALLOW
match
:
START
roles
:
-
'
read:pages'
...
...
@@ -142,6 +142,7 @@ groups:
-
'
write:comments'
path
:
'
'
locales
:
[]
sites
:
[]
reservedPaths
:
-
login
-
logout
...
...
server/graph/resolvers/group.js
View file @
6e303ac6
const
graphHelper
=
require
(
'../../helpers/graph'
)
const
safeRegex
=
require
(
'safe-regex'
)
const
_
=
require
(
'lodash'
)
const
gql
=
require
(
'graphql
'
)
const
{
v4
:
uuid
}
=
require
(
'uuid
'
)
/* global WIKI */
...
...
@@ -30,13 +30,13 @@ module.exports = {
async
assignUserToGroup
(
obj
,
args
,
{
req
})
{
// Check for guest user
if
(
args
.
userId
===
2
)
{
throw
new
gql
.
GraphQL
Error
(
'Cannot assign the Guest user to a group.'
)
throw
new
Error
(
'Cannot assign the Guest user to a group.'
)
}
// Check for valid group
const
grp
=
await
WIKI
.
models
.
groups
.
query
().
findById
(
args
.
groupId
)
if
(
!
grp
)
{
throw
new
gql
.
GraphQL
Error
(
'Invalid Group ID'
)
throw
new
Error
(
'Invalid Group ID'
)
}
// Check assigned permissions for write:groups
...
...
@@ -47,13 +47,13 @@ module.exports = {
return
[
'users'
,
'groups'
,
'navigation'
,
'theme'
,
'api'
,
'system'
].
includes
(
resType
)
})
)
{
throw
new
gql
.
GraphQL
Error
(
'You are not authorized to assign a user to this elevated group.'
)
throw
new
Error
(
'You are not authorized to assign a user to this elevated group.'
)
}
// Check for valid user
const
usr
=
await
WIKI
.
models
.
users
.
query
().
findById
(
args
.
userId
)
if
(
!
usr
)
{
throw
new
gql
.
GraphQL
Error
(
'Invalid User ID'
)
throw
new
Error
(
'Invalid User ID'
)
}
// Check for existing relation
...
...
@@ -62,7 +62,7 @@ module.exports = {
groupId
:
args
.
groupId
}).
first
()
if
(
relExist
)
{
throw
new
gql
.
GraphQL
Error
(
'User is already assigned to group.'
)
throw
new
Error
(
'User is already assigned to group.'
)
}
// Assign user to group
...
...
@@ -73,7 +73,7 @@ module.exports = {
WIKI
.
events
.
outbound
.
emit
(
'addAuthRevoke'
,
{
id
:
usr
.
id
,
kind
:
'u'
})
return
{
responseResult
:
graphHelper
.
generateSuccess
(
'User has been assigned to group.'
)
operation
:
graphHelper
.
generateSuccess
(
'User has been assigned to group.'
)
}
},
/**
...
...
@@ -83,13 +83,16 @@ module.exports = {
const
group
=
await
WIKI
.
models
.
groups
.
query
().
insertAndFetch
({
name
:
args
.
name
,
permissions
:
JSON
.
stringify
(
WIKI
.
data
.
groups
.
defaultPermissions
),
pageRules
:
JSON
.
stringify
(
WIKI
.
data
.
groups
.
defaultPageRules
),
rules
:
JSON
.
stringify
(
WIKI
.
data
.
groups
.
defaultRules
.
map
(
r
=>
({
id
:
uuid
(),
...
r
}))),
isSystem
:
false
})
await
WIKI
.
auth
.
reloadGroups
()
WIKI
.
events
.
outbound
.
emit
(
'reloadGroups'
)
return
{
responseResult
:
graphHelper
.
generateSuccess
(
'Group created successfully.'
),
operation
:
graphHelper
.
generateSuccess
(
'Group created successfully.'
),
group
}
},
...
...
@@ -98,7 +101,7 @@ module.exports = {
*/
async
deleteGroup
(
obj
,
args
)
{
if
(
args
.
id
===
1
||
args
.
id
===
2
)
{
throw
new
gql
.
GraphQL
Error
(
'Cannot delete this group.'
)
throw
new
Error
(
'Cannot delete this group.'
)
}
await
WIKI
.
models
.
groups
.
query
().
deleteById
(
args
.
id
)
...
...
@@ -110,7 +113,7 @@ module.exports = {
WIKI
.
events
.
outbound
.
emit
(
'reloadGroups'
)
return
{
responseResult
:
graphHelper
.
generateSuccess
(
'Group has been deleted.'
)
operation
:
graphHelper
.
generateSuccess
(
'Group has been deleted.'
)
}
},
/**
...
...
@@ -118,18 +121,18 @@ module.exports = {
*/
async
unassignUserFromGroup
(
obj
,
args
)
{
if
(
args
.
userId
===
2
)
{
throw
new
gql
.
GraphQL
Error
(
'Cannot unassign Guest user'
)
throw
new
Error
(
'Cannot unassign Guest user'
)
}
if
(
args
.
userId
===
1
&&
args
.
groupId
===
1
)
{
throw
new
gql
.
GraphQL
Error
(
'Cannot unassign Administrator user from Administrators group.'
)
throw
new
Error
(
'Cannot unassign Administrator user from Administrators group.'
)
}
const
grp
=
await
WIKI
.
models
.
groups
.
query
().
findById
(
args
.
groupId
)
if
(
!
grp
)
{
throw
new
gql
.
GraphQL
Error
(
'Invalid Group ID'
)
throw
new
Error
(
'Invalid Group ID'
)
}
const
usr
=
await
WIKI
.
models
.
users
.
query
().
findById
(
args
.
userId
)
if
(
!
usr
)
{
throw
new
gql
.
GraphQL
Error
(
'Invalid User ID'
)
throw
new
Error
(
'Invalid User ID'
)
}
await
grp
.
$relatedQuery
(
'users'
).
unrelate
().
where
(
'userId'
,
usr
.
id
)
...
...
@@ -137,7 +140,7 @@ module.exports = {
WIKI
.
events
.
outbound
.
emit
(
'addAuthRevoke'
,
{
id
:
usr
.
id
,
kind
:
'u'
})
return
{
responseResult
:
graphHelper
.
generateSuccess
(
'User has been unassigned from group.'
)
operation
:
graphHelper
.
generateSuccess
(
'User has been unassigned from group.'
)
}
},
/**
...
...
@@ -148,7 +151,7 @@ module.exports = {
if
(
_
.
some
(
args
.
pageRules
,
pr
=>
{
return
pr
.
match
===
'REGEX'
&&
!
safeRegex
(
pr
.
path
)
}))
{
throw
new
gql
.
GraphQL
Error
(
'Some Page Rules contains unsafe or exponential time regex.'
)
throw
new
Error
(
'Some Page Rules contains unsafe or exponential time regex.'
)
}
// Set default redirect on login value
...
...
@@ -164,7 +167,7 @@ module.exports = {
return
[
'users'
,
'groups'
,
'navigation'
,
'theme'
,
'api'
,
'system'
].
includes
(
resType
)
})
)
{
throw
new
gql
.
GraphQL
Error
(
'You are not authorized to manage this group or assign these permissions.'
)
throw
new
Error
(
'You are not authorized to manage this group or assign these permissions.'
)
}
// Check assigned permissions for manage:groups
...
...
@@ -172,7 +175,7 @@ module.exports = {
WIKI
.
auth
.
checkExclusiveAccess
(
req
.
user
,
[
'manage:groups'
],
[
'manage:system'
])
&&
args
.
permissions
.
some
(
p
=>
_
.
last
(
p
.
split
(
':'
))
===
'system'
)
)
{
throw
new
gql
.
GraphQL
Error
(
'You are not authorized to manage this group or assign the manage:system permissions.'
)
throw
new
Error
(
'You are not authorized to manage this group or assign the manage:system permissions.'
)
}
// Update group
...
...
@@ -192,7 +195,7 @@ module.exports = {
WIKI
.
events
.
outbound
.
emit
(
'reloadGroups'
)
return
{
responseResult
:
graphHelper
.
generateSuccess
(
'Group has been updated.'
)
operation
:
graphHelper
.
generateSuccess
(
'Group has been updated.'
)
}
}
},
...
...
ux/jsconfig.json
View file @
6e303ac6
{
"compilerOptions"
:
{
"baseUrl"
:
"."
,
"jsx"
:
"preserve"
,
"paths"
:
{
"src/*"
:
[
"src/*"
...
...
ux/src/components/GroupCreateDialog.vue
View file @
6e303ac6
<
template
lang=
"pug"
>
q-dialog(ref='dialog', @hide='onDialogHide')
q-dialog(ref='dialog
Ref
', @hide='onDialogHide')
q-card(style='min-width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
span
{{
$
t
(
`admin.groups.create`
)
}}
span
{{
t
(
`admin.groups.create`
)
}}
q-form.q-py-sm(ref='createGroupForm', @submit='create')
q-item
blueprint-icon(icon='team')
q-item-section
q-input(
outlined
v-model='groupName'
v-model='
state.
groupName'
dense
:rules=`[
val => val.length > 0 || $t('admin.groups.nameMissing'),
val => /^[^<>"]+$/.test(val) || $t('admin.groups.nameInvalidChars')
]`
:rules='groupNameValidation'
hide-bottom-space
:label='
$
t(`common.field.name`)'
:aria-label='
$
t(`common.field.name`)'
:label='t(`common.field.name`)'
:aria-label='t(`common.field.name`)'
lazy-rules='ondemand'
autofocus
)
...
...
@@ -26,50 +23,70 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-space
q-btn.acrylic-btn(
flat
:label='
$
t(`common.actions.cancel`)'
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='
hide
'
@click='
onDialogCancel
'
)
q-btn(
unelevated
:label='
$
t(`common.actions.create`)'
:label='t(`common.actions.create`)'
color='primary'
padding='xs md'
@click='create'
:loading='isLoading'
:loading='
state.
isLoading'
)
</
template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
{
useI18n
}
from
'vue-i18n'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
reactive
,
ref
}
from
'vue'
export
default
{
emits
:
[
'ok'
,
'hide'
],
data
()
{
return
{
// EMITS
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
groupName
:
''
,
isLoading
:
false
}
},
methods
:
{
show
()
{
this
.
$refs
.
dialog
.
show
()
},
hide
()
{
this
.
$refs
.
dialog
.
hide
()
},
onDialogHide
()
{
this
.
$emit
(
'hide'
)
},
async
create
()
{
this
.
isLoading
=
true
})
// REFS
const
createGroupForm
=
ref
(
null
)
// VALIDATION RULES
const
groupNameValidation
=
[
val
=>
val
.
length
>
0
||
t
(
'admin.groups.nameMissing'
),
val
=>
/^
[^
<>"
]
+$/
.
test
(
val
)
||
t
(
'admin.groups.nameInvalidChars'
)
]
// METHODS
async
function
create
()
{
state
.
isLoading
=
true
try
{
const
isFormValid
=
await
this
.
$refs
.
createGroupForm
.
validate
(
true
)
const
isFormValid
=
await
createGroupForm
.
value
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
this
.
$
t
(
'admin.groups.createInvalidData'
))
throw
new
Error
(
t
(
'admin.groups.createInvalidData'
))
}
const
resp
=
await
this
.
$apollo
.
mutate
({
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation createGroup (
$name: String!
...
...
@@ -77,7 +94,7 @@ export default {
createGroup(
name: $name
) {
status
{
operation
{
succeeded
message
}
...
...
@@ -85,27 +102,24 @@ export default {
}
`
,
variables
:
{
name
:
this
.
groupName
name
:
state
.
groupName
}
})
if
(
resp
?.
data
?.
createGroup
?.
status
?.
succeeded
)
{
this
.
$q
.
notify
({
if
(
resp
?.
data
?.
createGroup
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$
t
(
'admin.groups.createSuccess'
)
message
:
t
(
'admin.groups.createSuccess'
)
})
this
.
$emit
(
'ok'
)
this
.
hide
()
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
createGroup
?.
status
?.
message
||
'An unexpected error occured.'
)
throw
new
Error
(
resp
?.
data
?.
createGroup
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
this
.
isLoading
=
false
}
}
state
.
isLoading
=
false
}
</
script
>
ux/src/components/GroupDeleteDialog.vue
View file @
6e303ac6
<
template
lang=
"pug"
>
q-dialog(ref='dialog', @hide='onDialogHide')
q-dialog(ref='dialog
Ref
', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm')
span
{{
$
t
(
`admin.groups.delete`
)
}}
span
{{
t
(
`admin.groups.delete`
)
}}
q-card-section
.text-body2
i18n-t(keypath='admin.groups.deleteConfirm')
template(#groupName)
strong
{{
group
.
name
}}
strong
{{
props
.
group
.
name
}}
.text-body2.q-mt-md
strong.text-negative
{{
$
t
(
`admin.groups.deleteConfirmWarn`
)
}}
strong.text-negative
{{
t
(
`admin.groups.deleteConfirmWarn`
)
}}
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='
$
t(`common.actions.cancel`)'
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='
hide
'
@click='
onDialogCancel
'
)
q-btn(
unelevated
:label='
$
t(`common.actions.delete`)'
:label='t(`common.actions.delete`)'
color='negative'
padding='xs md'
@click='confirm'
)
</
template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
{
useI18n
}
from
'vue-i18n'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
export
default
{
props
:
{
// PROPS
const
props
=
defineProps
({
group
:
{
type
:
Object
,
required
:
true
}
},
emits
:
[
'ok'
,
'hide'
],
data
()
{
return
{
}
},
methods
:
{
show
()
{
this
.
$refs
.
dialog
.
show
()
},
hide
()
{
this
.
$refs
.
dialog
.
hide
()
},
onDialogHide
()
{
this
.
$emit
(
'hide'
)
},
async
confirm
()
{
})
// EMITS
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// I18N
const
{
t
}
=
useI18n
()
// METHODS
async
function
confirm
()
{
try
{
const
resp
=
await
this
.
$apollo
.
mutate
({
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation deleteGroup ($id: UUID!) {
deleteGroup(id: $id) {
status
{
operation
{
succeeded
message
}
...
...
@@ -68,26 +74,23 @@ export default {
}
`
,
variables
:
{
id
:
thi
s
.
group
.
id
id
:
prop
s
.
group
.
id
}
})
if
(
resp
?.
data
?.
deleteGroup
?.
status
?.
succeeded
)
{
this
.
$q
.
notify
({
if
(
resp
?.
data
?.
deleteGroup
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$
t
(
'admin.groups.deleteSuccess'
)
message
:
t
(
'admin.groups.deleteSuccess'
)
})
this
.
$emit
(
'ok'
)
this
.
hide
()
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
deleteGroup
?.
status
?.
message
||
'An unexpected error occured.'
)
throw
new
Error
(
resp
?.
data
?.
deleteGroup
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
}
}
}
</
script
>
ux/src/components/GroupEditOverlay.vue
View file @
6e303ac6
...
...
@@ -3,24 +3,24 @@ q-layout(view='hHh lpR fFf', container)
q-header.card-header.q-px-md.q-py-sm
q-icon(name='img:/_assets/icons/fluent-people.svg', left, size='md')
div
span
{{
$
t
(
`admin.groups.edit`
)
}}
.text-caption
{{
group
.
name
}}
span
{{
t
(
`admin.groups.edit`
)
}}
.text-caption
{{
state
.
group
.
name
}}
q-space
q-btn-group(push)
q-btn(
push
color='grey-6'
text-color='white'
:aria-label='
$
t(`common.actions.refresh`)'
:aria-label='t(`common.actions.refresh`)'
icon='las la-redo-alt'
@click='refresh'
)
q-tooltip(anchor='center left', self='center right')
{{
$
t
(
`common.actions.refresh`
)
}}
q-tooltip(anchor='center left', self='center right')
{{
t
(
`common.actions.refresh`
)
}}
q-btn(
push
color='white'
text-color='grey-7'
:label='
$
t(`common.actions.close`)'
:label='t(`common.actions.close`)'
icon='las la-times'
@click='close'
)
...
...
@@ -28,11 +28,11 @@ q-layout(view='hHh lpR fFf', container)
push
color='positive'
text-color='white'
:label='
$
t(`common.actions.save`)'
:label='t(`common.actions.save`)'
icon='las la-check'
)
q-drawer.bg-dark-6(:model-value='true', :width='250', dark)
q-list(padding, v-show='!isLoading')
q-list(padding, v-show='!
state.
isLoading')
q-item(
v-for='sc of sections'
:key='`section-` + sc.key'
...
...
@@ -45,109 +45,107 @@ q-layout(view='hHh lpR fFf', container)
q-icon(:name='sc.icon', color='white')
q-item-section
{{
sc
.
text
}}
q-item-section(side, v-if='sc.usersTotal')
q-badge(color='dark-3', :label='usersTotal')
q-item-section(side, v-if='sc.rulesTotal && group.rules')
q-badge(color='dark-3', :label='group.rules.length')
q-badge(color='dark-3', :label='
state.
usersTotal')
q-item-section(side, v-if='sc.rulesTotal &&
state.
group.rules')
q-badge(color='dark-3', :label='
state.
group.rules.length')
q-page-container
q-page(v-if='isLoading')
q-page(v-if='
state.
isLoading')
//- -----------------------------------------------------------------------
//- OVERVIEW
//- -----------------------------------------------------------------------
q-page(v-else-if='
$
route.params.section === `overview`')
q-page(v-else-if='route.params.section === `overview`')
.q-pa-md
.row.q-col-gutter-md
.col-12.col-lg-8
q-card.shadow-1.q-pb-sm
q-card-section
.text-subtitle1
{{
$
t
(
'admin.groups.general'
)
}}
.text-subtitle1
{{
t
(
'admin.groups.general'
)
}}
q-item
blueprint-icon(icon='team')
q-item-section
q-item-label
{{
$
t
(
`admin.groups.name`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.groups.nameHint`
)
}}
q-item-label
{{
t
(
`admin.groups.name`
)
}}
q-item-label(caption)
{{
t
(
`admin.groups.nameHint`
)
}}
q-item-section
q-input(
outlined
v-model='group.name'
v-model='
state.
group.name'
dense
:rules=`[
val => /^[^<>"]+$/.test(val) || $t('admin.groups.nameInvalidChars')
]`
:rules='groupNameValidation'
hide-bottom-space
:aria-label='
$
t(`admin.groups.name`)'
:aria-label='t(`admin.groups.name`)'
)
q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section
.text-subtitle1
{{
$
t
(
'admin.groups.authBehaviors'
)
}}
.text-subtitle1
{{
t
(
'admin.groups.authBehaviors'
)
}}
q-item
blueprint-icon(icon='double-right')
q-item-section
q-item-label
{{
$
t
(
`admin.groups.redirectOnLogin`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.groups.redirectOnLoginHint`
)
}}
q-item-label
{{
t
(
`admin.groups.redirectOnLogin`
)
}}
q-item-label(caption)
{{
t
(
`admin.groups.redirectOnLoginHint`
)
}}
q-item-section
q-input(
outlined
v-model='group.redirectOnLogin'
v-model='
state.
group.redirectOnLogin'
dense
:aria-label='
$
t(`admin.groups.redirectOnLogin`)'
:aria-label='t(`admin.groups.redirectOnLogin`)'
)
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='chevron-right')
q-item-section
q-item-label
{{
$
t
(
`admin.groups.redirectOnFirstLogin`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.groups.redirectOnFirstLoginHint`
)
}}
q-item-label
{{
t
(
`admin.groups.redirectOnFirstLogin`
)
}}
q-item-label(caption)
{{
t
(
`admin.groups.redirectOnFirstLoginHint`
)
}}
q-item-section
q-input(
outlined
v-model='group.redirectOnFirstLogin'
v-model='
state.
group.redirectOnFirstLogin'
dense
:aria-label='
$
t(`admin.groups.redirectOnLogin`)'
:aria-label='t(`admin.groups.redirectOnLogin`)'
)
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='exit')
q-item-section
q-item-label
{{
$
t
(
`admin.groups.redirectOnLogout`
)
}}
q-item-label(caption)
{{
$
t
(
`admin.groups.redirectOnLogoutHint`
)
}}
q-item-label
{{
t
(
`admin.groups.redirectOnLogout`
)
}}
q-item-label(caption)
{{
t
(
`admin.groups.redirectOnLogoutHint`
)
}}
q-item-section
q-input(
outlined
v-model='group.redirectOnLogout'
v-model='
state.
group.redirectOnLogout'
dense
:aria-label='
$
t(`admin.groups.redirectOnLogout`)'
:aria-label='t(`admin.groups.redirectOnLogout`)'
)
.col-12.col-lg-4
q-card.shadow-1.q-pb-sm
q-card-section
.text-subtitle1
{{
$
t
(
'admin.groups.info'
)
}}
.text-subtitle1
{{
t
(
'admin.groups.info'
)
}}
q-item
blueprint-icon(icon='team', :hue-rotate='-45')
q-item-section
q-item-label
{{
$
t
(
`common.field.id`
)
}}
q-item-label: strong
{{
groupI
d
}}
q-item-label
{{
t
(
`common.field.id`
)
}}
q-item-label: strong
{{
state
.
group
.
i
d
}}
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='calendar-plus', :hue-rotate='-45')
q-item-section
q-item-label
{{
$
t
(
`common.field.createdOn`
)
}}
q-item-label: strong
{{
humanizeDate
(
group
.
createdAt
)
}}
q-item-label
{{
t
(
`common.field.createdOn`
)
}}
q-item-label: strong
{{
humanizeDate
(
state
.
group
.
createdAt
)
}}
q-separator.q-my-sm(inset)
q-item
blueprint-icon(icon='summertime', :hue-rotate='-45')
q-item-section
q-item-label
{{
$
t
(
`common.field.lastUpdated`
)
}}
q-item-label: strong
{{
humanizeDate
(
group
.
updatedAt
)
}}
q-item-label
{{
t
(
`common.field.lastUpdated`
)
}}
q-item-label: strong
{{
humanizeDate
(
state
.
group
.
updatedAt
)
}}
//- -----------------------------------------------------------------------
//- RULES
//- -----------------------------------------------------------------------
q-page(v-else-if='
$
route.params.section === `rules`')
q-page(v-else-if='route.params.section === `rules`')
q-toolbar.q-pl-md(
:class='$q.dark.isActive ? `bg-dark-3` : `bg-white`'
)
.text-subtitle1
{{
$
t
(
'admin.groups.rules'
)
}}
.text-subtitle1
{{
t
(
'admin.groups.rules'
)
}}
q-space
q-btn.acrylic-btn.q-mr-sm(
icon='las la-question-circle'
...
...
@@ -163,14 +161,14 @@ q-layout(view='hHh lpR fFf', container)
icon='las la-file-export'
@click='exportRules'
)
q-tooltip
{{
$
t
(
'admin.groups.exportRules'
)
}}
q-tooltip
{{
t
(
'admin.groups.exportRules'
)
}}
q-btn.acrylic-btn.q-mr-sm(
flat
color='indigo'
icon='las la-file-import'
@click='importRules'
)
q-tooltip
{{
$
t
(
'admin.groups.importRules'
)
}}
q-tooltip
{{
t
(
'admin.groups.importRules'
)
}}
q-btn(
unelevated
color='primary'
...
...
@@ -181,14 +179,14 @@ q-layout(view='hHh lpR fFf', container)
q-separator
.q-pa-md
q-banner(
v-if='!
group.rules ||
group.rules.length < 1'
v-if='!
state.group.rules || state.
group.rules.length < 1'
rounded
:class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`'
)
{{
$
t
(
'admin.groups.rulesNone'
)
}}
)
{{
t
(
'admin.groups.rulesNone'
)
}}
q-card.shadow-1.q-pb-sm(v-else)
q-card-section
.admin-groups-rule(
v-for='(rule, idx) of group.rules'
v-for='(rule, idx) of
state.
group.rules'
:key='rule.id'
)
.admin-groups-rule-icon(:class='getRuleModeColor(rule.mode)')
...
...
@@ -213,7 +211,7 @@ q-layout(view='hHh lpR fFf', container)
emit-value
map-options
dense
:aria-label='
$
t(`admin.groups.ruleSites`)'
:aria-label='t(`admin.groups.ruleSites`)'
:options='rules'
placeholder='Select permissions...'
option-value='permission'
...
...
@@ -261,13 +259,13 @@ q-layout(view='hHh lpR fFf', container)
emit-value
map-options
dense
:aria-label='
$
t(`admin.groups.ruleSites`)'
:options='sites'
:aria-label='t(`admin.groups.ruleSites`)'
:options='
adminStore.
sites'
option-value='id'
option-label='title'
multiple
behavior='dialog'
:display-value='
$tc
(`admin.groups.selectedSites`, rule.sites.length, { count: rule.sites.length })'
:display-value='
t
(`admin.groups.selectedSites`, rule.sites.length, { count: rule.sites.length })'
)
template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }')
q-item(v-bind='itemProps', v-on='itemEvents')
...
...
@@ -288,13 +286,13 @@ q-layout(view='hHh lpR fFf', container)
emit-value
map-options
dense
:aria-label='
$
t(`admin.groups.ruleLocales`)'
:options='locales'
:aria-label='t(`admin.groups.ruleLocales`)'
:options='
adminStore.
locales'
option-value='code'
option-label='name'
multiple
behavior='dialog'
:display-value='
$tc
(`admin.groups.selectedLocales`, rule.locales.length, { count: rule.locales.length, locale: rule.locales.length === 1 ? rule.locales[0].toUpperCase() : `` })'
:display-value='
t
(`admin.groups.selectedLocales`, rule.locales.length, { count: rule.locales.length, locale: rule.locales.length === 1 ? rule.locales[0].toUpperCase() : `` })'
)
template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
q-item(v-bind='itemProps')
...
...
@@ -317,14 +315,14 @@ q-layout(view='hHh lpR fFf', container)
emit-value
map-options
dense
:aria-label='
$
t(`admin.groups.ruleMatch`)'
:aria-label='t(`admin.groups.ruleMatch`)'
:options=`[
{ label:
$
t('admin.groups.ruleMatchStart'), value: 'START' },
{ label:
$
t('admin.groups.ruleMatchEnd'), value: 'END' },
{ label:
$
t('admin.groups.ruleMatchRegex'), value: 'REGEX' },
{ label:
$
t('admin.groups.ruleMatchTag'), value: 'TAG' },
{ label:
$
t('admin.groups.ruleMatchTagAll'), value: 'TAGALL' },
{ label:
$
t('admin.groups.ruleMatchExact'), value: 'EXACT' }
{ label: t('admin.groups.ruleMatchStart'), value: 'START' },
{ label: t('admin.groups.ruleMatchEnd'), value: 'END' },
{ label: t('admin.groups.ruleMatchRegex'), value: 'REGEX' },
{ label: t('admin.groups.ruleMatchTag'), value: 'TAG' },
{ label: t('admin.groups.ruleMatchTagAll'), value: 'TAGALL' },
{ label: t('admin.groups.ruleMatchExact'), value: 'EXACT' }
]`
)
q-input.q-mt-sm(
...
...
@@ -333,19 +331,19 @@ q-layout(view='hHh lpR fFf', container)
dense
:prefix='[`START`, `REGEX`, `EXACT`].includes(rule.match) ? `/` : null'
:suffix='rule.match === `REGEX` ? `/` : null'
:aria-label='
$
t(`admin.groups.rulePath`)'
:aria-label='t(`admin.groups.rulePath`)'
)
//- -----------------------------------------------------------------------
//- PERMISSIONS
//- -----------------------------------------------------------------------
q-page(v-else-if='
$
route.params.section === `permissions`')
q-page(v-else-if='route.params.section === `permissions`')
.q-pa-md
.row.q-col-gutter-md
.col-12.col-lg-6
q-card.shadow-1.q-pb-sm
.flex.justify-between
q-card-section
.text-subtitle1
{{
$
t
(
`admin.groups.permissions`
)
}}
.text-subtitle1
{{
t
(
`admin.groups.permissions`
)
}}
q-card-section
q-btn.acrylic-btn(
icon='las la-question-circle'
...
...
@@ -368,22 +366,22 @@ q-layout(view='hHh lpR fFf', container)
q-item-label(caption)
{{
perm
.
hint
}}
q-item-section(avatar)
q-toggle(
v-model='group.permissions'
v-model='
state.
group.permissions'
:val='perm.permission'
color='primary'
checked-icon='las la-check'
unchecked-icon='las la-times'
:aria-label='
$
t(`admin.general.allowComments`)'
:aria-label='t(`admin.general.allowComments`)'
)
q-separator.q-my-sm(inset, v-if='idx < permissions.length - 1')
//- -----------------------------------------------------------------------
//- USERS
//- -----------------------------------------------------------------------
q-page(v-else-if='
$
route.params.section === `users`')
q-page(v-else-if='route.params.section === `users`')
q-toolbar(
:class='$q.dark.isActive ? `bg-dark-3` : `bg-white`'
)
.text-subtitle1
{{
$
t
(
'admin.groups.users'
)
}}
.text-subtitle1
{{
t
(
'admin.groups.users'
)
}}
q-space
q-btn.acrylic-btn.q-mr-sm(
icon='las la-question-circle'
...
...
@@ -395,8 +393,8 @@ q-layout(view='hHh lpR fFf', container)
)
q-input.denser.fill-outline.q-mr-sm(
outlined
v-model='usersFilter'
:placeholder='
$
t(`admin.groups.filterUsers`)'
v-model='
state.
usersFilter'
:placeholder='t(`admin.groups.filterUsers`)'
dense
)
template(#prepend)
...
...
@@ -410,22 +408,27 @@ q-layout(view='hHh lpR fFf', container)
q-btn.q-mr-xs(
unelevated
icon='las la-user-plus'
:label='
$
t(`admin.groups.assignUser`)'
:label='t(`admin.groups.assignUser`)'
color='primary'
@click='assignUser'
)
q-separator
.q-pa-md
q-banner(
v-if='!state.users || state.users.length < 1'
rounded
:class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`'
)
{{
t
(
'admin.groups.usersNone'
)
}}
q-card.shadow-1
q-table(
:rows='users'
:rows='
state.
users'
:columns='usersHeaders'
row-key='id'
flat
hide-header
hide-bottom
:rows-per-page-options='[0]'
:loading='isLoadingUsers'
:loading='
state.
isLoadingUsers'
)
template(v-slot:body-cell-id='props')
q-td(:props='props')
...
...
@@ -467,7 +470,7 @@ q-layout(view='hHh lpR fFf', container)
:to='`/_admin/users/` + props.row.id'
icon='las la-pen'
color='indigo'
:label='
$
t(`common.actions.edit`)'
:label='t(`common.actions.edit`)'
no-caps
)
q-btn.acrylic-btn(
...
...
@@ -480,7 +483,7 @@ q-layout(view='hHh lpR fFf', container)
.flex.flex-center.q-mt-md(v-if='usersTotalPages > 1')
q-pagination(
v-model='usersPage'
v-model='
state.
usersPage'
:max='usersTotalPages'
:max-pages='9'
boundary-numbers
...
...
@@ -488,31 +491,159 @@ q-layout(view='hHh lpR fFf', container)
)
</
template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
{
get
}
from
'vuex-pathify'
import
{
DateTime
}
from
'luxon'
import
cloneDeep
from
'lodash/cloneDeep'
import
some
from
'lodash/some'
import
{
v4
as
uuid
}
from
'uuid'
import
{
exportFile
}
from
'quasar'
import
{
fileOpen
}
from
'browser-fs-access'
export
default
{
data
()
{
return
{
sections
:
[
{
key
:
'overview'
,
text
:
this
.
$t
(
'admin.groups.overview'
),
icon
:
'las la-users'
},
{
key
:
'rules'
,
text
:
this
.
$t
(
'admin.groups.rules'
),
icon
:
'las la-file-invoice'
,
rulesTotal
:
true
},
{
key
:
'permissions'
,
text
:
this
.
$t
(
'admin.groups.permissions'
),
icon
:
'las la-list-alt'
},
{
key
:
'users'
,
text
:
this
.
$t
(
'admin.groups.users'
),
icon
:
'las la-user'
,
usersTotal
:
true
}
],
import
{
useI18n
}
from
'vue-i18n'
import
{
exportFile
,
useQuasar
}
from
'quasar'
import
{
computed
,
onBeforeUnmount
,
onMounted
,
reactive
,
watch
}
from
'vue'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useAdminStore
}
from
'src/stores/admin'
// QUASAR
const
$q
=
useQuasar
()
// STORES
const
adminStore
=
useAdminStore
()
// ROUTER
const
router
=
useRouter
()
const
route
=
useRoute
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
group
:
{
rules
:
[]
},
isLoading
:
false
,
// RULES
rules
:
[
users
:
[],
isLoadingUsers
:
false
,
usersFilter
:
''
,
usersPage
:
1
,
usersPageSize
:
15
,
usersTotal
:
0
})
const
sections
=
[
{
key
:
'overview'
,
text
:
t
(
'admin.groups.overview'
),
icon
:
'las la-users'
},
{
key
:
'rules'
,
text
:
t
(
'admin.groups.rules'
),
icon
:
'las la-file-invoice'
,
rulesTotal
:
true
},
{
key
:
'permissions'
,
text
:
t
(
'admin.groups.permissions'
),
icon
:
'las la-list-alt'
},
{
key
:
'users'
,
text
:
t
(
'admin.groups.users'
),
icon
:
'las la-user'
,
usersTotal
:
true
}
]
const
usersHeaders
=
[
{
align
:
'center'
,
field
:
'id'
,
name
:
'id'
,
sortable
:
false
,
style
:
'width: 20px'
},
{
label
:
t
(
'common.field.name'
),
align
:
'left'
,
field
:
'name'
,
name
:
'name'
,
sortable
:
true
},
{
label
:
t
(
'admin.users.email'
),
align
:
'left'
,
field
:
'email'
,
name
:
'email'
,
sortable
:
false
},
{
align
:
'left'
,
field
:
'createdAt'
,
name
:
'date'
,
sortable
:
false
},
{
label
:
''
,
align
:
'right'
,
field
:
'edit'
,
name
:
'edit'
,
sortable
:
false
,
style
:
'width: 250px'
}
]
const
permissions
=
[
{
permission
:
'write:users'
,
hint
:
'Can create or authorize new users, but not modify existing ones'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:users'
,
hint
:
'Can manage all users (but not users with administrative permissions)'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'write:groups'
,
hint
:
'Can manage groups and assign CONTENT permissions / page rules'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:groups'
,
hint
:
'Can manage groups and assign ANY permissions (but not manage:system) / page rules'
,
warning
:
true
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:navigation'
,
hint
:
'Can manage the site navigation'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:theme'
,
hint
:
'Can manage and modify themes'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:api'
,
hint
:
'Can generate and revoke API keys'
,
warning
:
true
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:system'
,
hint
:
'Can manage and access everything. Root administrator.'
,
warning
:
true
,
restrictedForSystem
:
true
,
disabled
:
true
}
]
const
rules
=
[
{
permission
:
'read:pages'
,
title
:
'Read Pages'
,
...
...
@@ -633,183 +764,90 @@ export default {
restrictedForSystem
:
true
,
disabled
:
false
}
],
// PERMISSIONS
permissions
:
[
{
permission
:
'write:users'
,
hint
:
'Can create or authorize new users, but not modify existing ones'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:users'
,
hint
:
'Can manage all users (but not users with administrative permissions)'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'write:groups'
,
hint
:
'Can manage groups and assign CONTENT permissions / page rules'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:groups'
,
hint
:
'Can manage groups and assign ANY permissions (but not manage:system) / page rules'
,
warning
:
true
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:navigation'
,
hint
:
'Can manage the site navigation'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:theme'
,
hint
:
'Can manage and modify themes'
,
warning
:
false
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:api'
,
hint
:
'Can generate and revoke API keys'
,
warning
:
true
,
restrictedForSystem
:
true
,
disabled
:
false
},
{
permission
:
'manage:system'
,
hint
:
'Can manage and access everything. Root administrator.'
,
warning
:
true
,
restrictedForSystem
:
true
,
disabled
:
true
]
// VALIDATION RULES
const
groupNameValidation
=
[
val
=>
/^
[^
<>"
]
+$/
.
test
(
val
)
||
t
(
'admin.groups.nameInvalidChars'
)
]
// COMPUTED
const
usersTotalPages
=
computed
(()
=>
{
if
(
state
.
usersTotal
<
1
)
{
return
0
}
return
Math
.
ceil
(
state
.
usersTotal
/
state
.
usersPageSize
)
})
// WATCHERS
watch
(()
=>
route
.
params
.
section
,
checkRoute
)
watch
([()
=>
state
.
usersPage
,
()
=>
state
.
usersFilter
],
refreshUsers
)
// METHODS
function
close
()
{
adminStore
.
$patch
({
overlay
:
''
})
}
function
checkRoute
()
{
if
(
!
route
.
params
.
section
)
{
router
.
replace
({
params
:
{
section
:
'overview'
}
})
}
else
if
(
route
.
params
.
section
===
'users'
)
{
refreshUsers
()
}
],
// USERS
users
:
[],
isLoadingUsers
:
false
,
usersFilter
:
''
,
usersPage
:
1
,
usersPageSize
:
15
,
usersTotal
:
0
,
usersHeaders
:
[
{
align
:
'center'
,
field
:
'id'
,
name
:
'id'
,
sortable
:
false
,
style
:
'width: 20px'
},
{
label
:
this
.
$t
(
'common.field.name'
),
align
:
'left'
,
field
:
'name'
,
name
:
'name'
,
sortable
:
true
},
{
label
:
this
.
$t
(
'admin.users.email'
),
align
:
'left'
,
field
:
'email'
,
name
:
'email'
,
sortable
:
false
},
{
align
:
'left'
,
field
:
'createdAt'
,
name
:
'date'
,
sortable
:
false
},
{
label
:
''
,
align
:
'right'
,
field
:
'edit'
,
name
:
'edit'
,
sortable
:
false
,
style
:
'width: 250px'
}
]
}
},
computed
:
{
groupId
:
get
(
'admin/overlayOpts@id'
,
false
),
sites
:
get
(
'admin/sites'
,
false
),
locales
:
get
(
'admin/locales'
,
false
),
usersTotalPages
()
{
if
(
this
.
usersTotal
<
1
)
{
return
0
}
return
Math
.
ceil
(
this
.
usersTotal
/
this
.
usersPageSize
)
}
},
watch
:
{
$route
:
'checkRoute'
,
usersPage
()
{
this
.
refreshUsers
()
},
usersFilter
()
{
this
.
refreshUsers
()
}
},
mounted
()
{
this
.
checkRoute
()
this
.
fetchGroup
()
},
methods
:
{
close
()
{
this
.
$store
.
set
(
'admin/overlay'
,
''
)
},
checkRoute
()
{
if
(
!
this
.
$route
.
params
.
section
)
{
this
.
$router
.
replace
({
params
:
{
section
:
'overview'
}
})
}
else
if
(
this
.
$route
.
params
.
section
===
'users'
)
{
this
.
refreshUsers
()
}
},
humanizeDate
(
val
)
{
}
function
humanizeDate
(
val
)
{
if
(
!
val
)
{
return
'---'
}
return
DateTime
.
fromISO
(
val
).
toLocaleString
(
DateTime
.
DATETIME_FULL
)
},
getRuleModeColor
:
(
mode
)
=>
({
}
function
getRuleModeColor
(
mode
)
{
return
({
DENY
:
'text-negative'
,
ALLOW
:
'text-positive'
,
FORCEALLOW
:
'text-blue'
})[
mode
],
getRuleModeClass
(
mode
)
{
})[
mode
]
}
function
getRuleModeClass
(
mode
)
{
return
'is-'
+
mode
.
toLowerCase
()
},
getRuleModeIcon
:
(
mode
)
=>
({
}
function
getRuleModeIcon
(
mode
)
{
return
({
DENY
:
'las la-ban'
,
ALLOW
:
'las la-check'
,
FORCEALLOW
:
'las la-check-double'
})[
mode
]
||
'las la-frog'
,
getNextRuleMode
:
(
mode
)
=>
({
})[
mode
]
||
'las la-frog'
}
function
getNextRuleMode
(
mode
)
{
return
({
DENY
:
'FORCEALLOW'
,
ALLOW
:
'DENY'
,
FORCEALLOW
:
'ALLOW'
})[
mode
]
||
'ALLOW'
,
getRuleModeName
(
mode
)
{
})[
mode
]
||
'ALLOW'
}
function
getRuleModeName
(
mode
)
{
switch
(
mode
)
{
case
'ALLOW'
:
return
this
.
$
t
(
'admin.groups.ruleAllow'
)
case
'DENY'
:
return
this
.
$
t
(
'admin.groups.ruleDeny'
)
case
'FORCEALLOW'
:
return
this
.
$
t
(
'admin.groups.ruleForceAllow'
)
case
'ALLOW'
:
return
t
(
'admin.groups.ruleAllow'
)
case
'DENY'
:
return
t
(
'admin.groups.ruleDeny'
)
case
'FORCEALLOW'
:
return
t
(
'admin.groups.ruleForceAllow'
)
default
:
return
'???'
}
},
refresh
()
{
this
.
fetchGroup
()
},
async
fetchGroup
()
{
this
.
isLoading
=
true
}
function
refresh
()
{
fetchGroup
()
}
async
function
fetchGroup
()
{
state
.
isLoading
=
true
try
{
const
resp
=
await
this
.
$apollo
.
query
({
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query adminFetchGroup (
$id: UUID!
...
...
@@ -841,28 +879,29 @@ export default {
}
`
,
variables
:
{
id
:
this
.
groupI
d
id
:
adminStore
.
overlayOpts
.
i
d
},
fetchPolicy
:
'network-only'
})
if
(
resp
?.
data
?.
groupById
)
{
this
.
group
=
cloneDeep
(
resp
.
data
.
groupById
)
this
.
usersTotal
=
this
.
group
.
userCount
??
0
state
.
group
=
cloneDeep
(
resp
.
data
.
groupById
)
state
.
usersTotal
=
state
.
group
.
userCount
??
0
}
else
{
throw
new
Error
(
'An unexpected error occured while fetching group details.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
this
.
isLoading
=
false
},
newRule
()
{
this
.
group
.
rules
.
push
({
state
.
isLoading
=
false
}
function
newRule
()
{
state
.
group
.
rules
.
push
({
id
:
uuid
(),
name
:
this
.
$
t
(
'admin.groups.ruleUntitled'
),
name
:
t
(
'admin.groups.ruleUntitled'
),
mode
:
'ALLOW'
,
match
:
'START'
,
roles
:
[],
...
...
@@ -870,20 +909,23 @@ export default {
locales
:
[],
sites
:
[]
})
},
deleteRule
(
id
)
{
this
.
group
.
rules
=
this
.
group
.
rules
.
filter
(
r
=>
r
.
id
!==
id
)
},
exportRules
()
{
if
(
this
.
group
.
rules
.
length
<
1
)
{
return
this
.
$q
.
notify
({
}
function
deleteRule
(
id
)
{
state
.
group
.
rules
=
state
.
group
.
rules
.
filter
(
r
=>
r
.
id
!==
id
)
}
function
exportRules
()
{
if
(
state
.
group
.
rules
.
length
<
1
)
{
return
$q
.
notify
({
type
:
'negative'
,
message
:
this
.
$
t
(
'admin.groups.exportRulesNoneError'
)
message
:
t
(
'admin.groups.exportRulesNoneError'
)
})
}
exportFile
(
'rules.json'
,
JSON
.
stringify
(
this
.
group
.
rules
,
null
,
2
),
{
mimeType
:
'application/json;charset=UTF-8'
})
},
async
importRules
()
{
exportFile
(
'rules.json'
,
JSON
.
stringify
(
state
.
group
.
rules
,
null
,
2
),
{
mimeType
:
'application/json;charset=UTF-8'
})
}
async
function
importRules
()
{
try
{
const
blob
=
await
fileOpen
({
mimeTypes
:
[
'application/json'
],
...
...
@@ -896,51 +938,52 @@ export default {
if
(
!
Array
.
isArray
(
rules
)
||
rules
.
length
<
1
)
{
throw
new
Error
(
'Invalid Rules Format'
)
}
this
.
$q
.
dialog
({
title
:
this
.
$
t
(
'admin.groups.importModeTitle'
),
message
:
this
.
$
t
(
'admin.groups.importModeText'
),
$q
.
dialog
({
title
:
t
(
'admin.groups.importModeTitle'
),
message
:
t
(
'admin.groups.importModeText'
),
options
:
{
model
:
'replace'
,
type
:
'radio'
,
items
:
[
{
label
:
this
.
$
t
(
'admin.groups.importModeReplace'
),
value
:
'replace'
},
{
label
:
this
.
$
t
(
'admin.groups.importModeAdd'
),
value
:
'add'
}
{
label
:
t
(
'admin.groups.importModeReplace'
),
value
:
'replace'
},
{
label
:
t
(
'admin.groups.importModeAdd'
),
value
:
'add'
}
]
},
persistent
:
true
}).
onOk
(
choice
=>
{
if
(
choice
===
'replace'
)
{
this
.
group
.
rules
=
[]
state
.
group
.
rules
=
[]
}
this
.
group
.
rules
=
[
...
this
.
group
.
rules
,
state
.
group
.
rules
=
[
...
state
.
group
.
rules
,
...
rules
.
map
(
r
=>
({
id
:
uuid
(),
name
:
r
.
name
||
this
.
$
t
(
'admin.groups.ruleUntitled'
),
name
:
r
.
name
||
t
(
'admin.groups.ruleUntitled'
),
mode
:
[
'ALLOW'
,
'DENY'
,
'FORCEALLOW'
].
includes
(
r
.
mode
)
?
r
.
mode
:
'DENY'
,
match
:
[
'START'
,
'END'
,
'REGEX'
,
'TAG'
,
'TAGALL'
,
'EXACT'
].
includes
(
r
.
match
)
?
r
.
match
:
'START'
,
roles
:
r
.
roles
||
[],
path
:
r
.
path
||
''
,
locales
:
r
.
locales
.
filter
(
l
=>
some
(
this
.
locales
,
[
'code'
,
l
])),
sites
:
r
.
sites
.
filter
(
s
=>
some
(
this
.
sites
,
[
'id'
,
s
]))
locales
:
r
.
locales
.
filter
(
l
=>
some
(
adminStore
.
locales
,
[
'code'
,
l
])),
sites
:
r
.
sites
.
filter
(
s
=>
some
(
adminStore
.
sites
,
[
'id'
,
s
]))
}))
]
this
.
$q
.
notify
({
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$
t
(
'admin.groups.importSuccess'
)
message
:
t
(
'admin.groups.importSuccess'
)
})
})
}
catch
(
err
)
{
this
.
$q
.
notify
({
$q
.
notify
({
type
:
'negative'
,
message
:
this
.
$
t
(
'admin.groups.importFailed'
)
+
` [
${
err
.
message
}
]`
message
:
t
(
'admin.groups.importFailed'
)
+
` [
${
err
.
message
}
]`
})
}
},
async
refreshUsers
()
{
this
.
isLoadingUsers
=
true
}
async
function
refreshUsers
()
{
state
.
isLoadingUsers
=
true
try
{
const
resp
=
await
this
.
$apollo
.
query
({
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query adminFetchGroupUsers (
$filter: String
...
...
@@ -970,35 +1013,43 @@ export default {
}
`
,
variables
:
{
filter
:
this
.
usersFilter
,
page
:
this
.
usersPage
,
pageSize
:
this
.
usersPageSize
,
groupId
:
this
.
groupI
d
filter
:
state
.
usersFilter
,
page
:
state
.
usersPage
,
pageSize
:
state
.
usersPageSize
,
groupId
:
adminStore
.
overlayOpts
.
i
d
},
fetchPolicy
:
'network-only'
})
if
(
resp
?.
data
?.
groupById
?.
users
)
{
this
.
usersTotal
=
resp
.
data
.
groupById
.
userCount
??
0
this
.
users
=
cloneDeep
(
resp
.
data
.
groupById
.
users
)
state
.
usersTotal
=
resp
.
data
.
groupById
.
userCount
??
0
state
.
users
=
cloneDeep
(
resp
.
data
.
groupById
.
users
)
}
else
{
throw
new
Error
(
'An unexpected error occured while fetching group users.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
this
.
isLoadingUsers
=
false
},
assignUser
()
{
state
.
isLoadingUsers
=
false
}
},
unassignUser
()
{
function
assignUser
()
{
}
function
unassignUser
()
{
}
}
}
// MOUNTED
onMounted
(()
=>
{
checkRoute
()
fetchGroup
()
})
</
script
>
<
style
lang=
"scss"
>
...
...
ux/src/components/SiteCreateDialog.vue
View file @
6e303ac6
...
...
@@ -12,10 +12,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
outlined
v-model='state.siteName'
dense
:rules=`[
val => val.length > 0 || t('admin.sites.nameMissing'),
val => /^[^<>"]+$/.test(val) || t('admin.sites.nameInvalidChars')
]`
:rules='siteNameValidation'
hide-bottom-space
:label='t(`common.field.name`)'
:aria-label='t(`common.field.name`)'
...
...
@@ -29,10 +26,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
outlined
v-model='state.siteHostname'
dense
:rules=`[
val => val.length > 0 || t('admin.sites.hostnameMissing'),
val => /^(\\*)|([a-z0-9\-.:]+)$/.test(val) || t('admin.sites.hostnameInvalidChars')
]`
:rules='siteHostnameValidation'
:hint='t(`admin.sites.hostnameHint`)'
hide-bottom-space
:label='t(`admin.sites.hostname`)'
...
...
@@ -97,6 +91,17 @@ const state = reactive({
const
createSiteForm
=
ref
(
null
)
// VALIDATION RULES
const
siteNameValidation
=
[
val
=>
val
.
length
>
0
||
t
(
'admin.sites.nameMissing'
),
val
=>
/^
[^
<>"
]
+$/
.
test
(
val
)
||
t
(
'admin.sites.nameInvalidChars'
)
]
const
siteHostnameValidation
=
[
val
=>
val
.
length
>
0
||
t
(
'admin.sites.hostnameMissing'
),
val
=>
/^
(\\
*
)
|
([
a-z0-9
\-
.:
]
+
)
$/
.
test
(
val
)
||
t
(
'admin.sites.hostnameInvalidChars'
)
]
// METHODS
async
function
create
()
{
...
...
ux/src/components/SiteDeleteDialog.vue
View file @
6e303ac6
...
...
@@ -81,7 +81,7 @@ async function confirm () {
mutation
:
gql
`
mutation deleteSite ($id: UUID!) {
deleteSite(id: $id) {
status
{
operation
{
succeeded
message
}
...
...
@@ -92,7 +92,7 @@ async function confirm () {
id
:
props
.
site
.
id
}
})
if
(
resp
?.
data
?.
deleteSite
?.
status
?.
succeeded
)
{
if
(
resp
?.
data
?.
deleteSite
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.sites.deleteSuccess'
)
...
...
@@ -102,7 +102,7 @@ async function confirm () {
})
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
deleteSite
?.
status
?.
message
||
'An unexpected error occured.'
)
throw
new
Error
(
resp
?.
data
?.
deleteSite
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
$q
.
notify
({
...
...
ux/src/i18n/locales/en.json
View file @
6e303ac6
...
...
@@ -1421,5 +1421,6 @@
"tags.searchWithinResultsPlaceholder"
:
"Search within results..."
,
"tags.selectOneMoreTags"
:
"Select one or more tags"
,
"tags.selectOneMoreTagsHint"
:
"Select one or more tags on the left."
,
"admin.general.sitemapHint"
:
"Make a sitemap.xml available to search engines with all pages accessible to guests."
"admin.general.sitemapHint"
:
"Make a sitemap.xml available to search engines with all pages accessible to guests."
,
"admin.groups.usersNone"
:
"This group doesn't have any user yet."
}
ux/src/layouts/AdminLayout.vue
View file @
6e303ac6
...
...
@@ -177,7 +177,7 @@ q-layout.admin(view='hHh Lpr lff')
transition-show='jump-up'
transition-hide='jump-down'
)
component(:is='
adminStore.overlay
')
component(:is='
overlays[adminStore.overlay]
')
q-footer.admin-footer
q-bar.justify-center(dense)
span(style='font-size: 11px;') Powered by #[a(href='https://js.wiki', target='_blank'): strong Wiki.js], an open source project.
...
...
@@ -195,8 +195,10 @@ import { useSiteStore } from '../stores/site'
// COMPONENTS
import
AccountMenu
from
'../components/AccountMenu.vue'
const
GroupEditOverlay
=
defineAsyncComponent
(()
=>
import
(
'../components/GroupEditOverlay.vue'
))
const
UserEditOverlay
=
defineAsyncComponent
(()
=>
import
(
'../components/UserEditOverlay.vue'
))
const
overlays
=
{
GroupEditOverlay
:
defineAsyncComponent
(()
=>
import
(
'../components/GroupEditOverlay.vue'
)),
UserEditOverlay
:
defineAsyncComponent
(()
=>
import
(
'../components/UserEditOverlay.vue'
))
}
// STORES
...
...
ux/src/pages/AdminGroups.vue
View file @
6e303ac6
...
...
@@ -175,7 +175,7 @@ watch(() => adminStore.overlay, (newValue, oldValue) => {
}
}
)
watch
(()
=>
route
,
()
=>
{
watch
(()
=>
route
.
params
.
id
,
()
=>
{
checkOverlay
()
}
)
...
...
@@ -213,7 +213,7 @@ async function load () {
}
function
checkOverlay
()
{
if
(
route
.
params
&&
route
.
params
.
id
)
{
if
(
route
.
params
?
.
id
)
{
adminStore
.
$patch
({
overlayOpts
:
{
id
:
route
.
params
.
id
}
,
overlay
:
'GroupEditOverlay'
...
...
ux/src/pages/AdminStorage.vue
View file @
6e303ac6
...
...
@@ -737,7 +737,7 @@ watch(() => state.targets, (newValue) => {
handleSetupCallback
()
}
})
watch
(()
=>
route
,
(
to
,
from
)
=>
{
watch
(()
=>
route
.
params
.
id
,
(
to
,
from
)
=>
{
if
(
!
to
.
params
.
id
)
{
return
}
...
...
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