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
5a4a9df4
Unverified
Commit
5a4a9df4
authored
Jun 18, 2022
by
Nicolas Giard
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(admin): migrate webhooks to vue 3 composable
parent
cc506a08
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
654 additions
and
416 deletions
+654
-416
hooks.js
server/graph/resolvers/hooks.js
+85
-0
hooks.graphql
server/graph/schemas/hooks.graphql
+70
-0
hooks.js
server/models/hooks.js
+44
-0
WebhookDeleteDialog.vue
ux/src/components/WebhookDeleteDialog.vue
+71
-57
WebhookEditDialog.vue
ux/src/components/WebhookEditDialog.vue
+281
-263
AdminLocale.vue
ux/src/pages/AdminLocale.vue
+2
-2
AdminWebhooks.vue
ux/src/pages/AdminWebhooks.vue
+100
-93
routes.js
ux/src/router/routes.js
+1
-1
No files found.
server/graph/resolvers/hooks.js
0 → 100644
View file @
5a4a9df4
const
graphHelper
=
require
(
'../../helpers/graph'
)
const
_
=
require
(
'lodash'
)
/* global WIKI */
module
.
exports
=
{
Query
:
{
async
hooks
()
{
return
WIKI
.
models
.
hooks
.
query
().
orderBy
(
'name'
)
},
async
hookById
(
obj
,
args
)
{
return
WIKI
.
models
.
hooks
.
query
().
findById
(
args
.
id
)
}
},
Mutation
:
{
/**
* CREATE HOOK
*/
async
createHook
(
obj
,
args
)
{
try
{
// -> Validate inputs
if
(
!
args
.
name
||
args
.
name
.
length
<
1
)
{
throw
WIKI
.
ERROR
(
new
Error
(
'Invalid Hook Name'
),
'HookCreateInvalidName'
)
}
if
(
!
args
.
events
||
args
.
events
.
length
<
1
)
{
throw
WIKI
.
ERROR
(
new
Error
(
'Invalid Hook Events'
),
'HookCreateInvalidEvents'
)
}
if
(
!
args
.
url
||
args
.
url
.
length
<
8
||
!
args
.
url
.
startsWith
(
'http'
))
{
throw
WIKI
.
ERROR
(
new
Error
(
'Invalid Hook URL'
),
'HookCreateInvalidURL'
)
}
// -> Create hook
const
newHook
=
await
WIKI
.
models
.
hooks
.
createHook
(
args
)
return
{
operation
:
graphHelper
.
generateSuccess
(
'Hook created successfully'
),
hook
:
newHook
}
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
}
},
/**
* UPDATE HOOK
*/
async
updateHook
(
obj
,
args
)
{
try
{
// -> Load hook
const
hook
=
await
WIKI
.
models
.
hooks
.
query
().
findById
(
args
.
id
)
if
(
!
hook
)
{
throw
WIKI
.
ERROR
(
new
Error
(
'Invalid Hook ID'
),
'HookInvalidId'
)
}
// -> Check for bad input
if
(
_
.
has
(
args
.
patch
,
'name'
)
&&
args
.
patch
.
name
.
length
<
1
)
{
throw
WIKI
.
ERROR
(
new
Error
(
'Invalid Hook Name'
),
'HookCreateInvalidName'
)
}
if
(
_
.
has
(
args
.
patch
,
'events'
)
&&
args
.
patch
.
events
.
length
<
1
)
{
throw
WIKI
.
ERROR
(
new
Error
(
'Invalid Hook Events'
),
'HookCreateInvalidEvents'
)
}
if
(
_
.
has
(
args
.
patch
,
'url'
)
&&
(
_
.
trim
(
args
.
patch
.
url
).
length
<
8
||
!
args
.
patch
.
url
.
startsWith
(
'http'
)))
{
throw
WIKI
.
ERROR
(
new
Error
(
'URL is invalid.'
),
'HookInvalidURL'
)
}
// -> Update hook
await
WIKI
.
models
.
hooks
.
query
().
findById
(
args
.
id
).
patch
(
args
.
patch
)
return
{
operation
:
graphHelper
.
generateSuccess
(
'Hook updated successfully'
)
}
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
}
},
/**
* DELETE HOOK
*/
async
deleteHook
(
obj
,
args
)
{
try
{
await
WIKI
.
models
.
hooks
.
deleteHook
(
args
.
id
)
return
{
operation
:
graphHelper
.
generateSuccess
(
'Hook deleted successfully'
)
}
}
catch
(
err
)
{
return
graphHelper
.
generateError
(
err
)
}
}
}
}
server/graph/schemas/hooks.graphql
0 → 100644
View file @
5a4a9df4
# ===============================================
# WEBHOOKS
# ===============================================
extend
type
Query
{
hooks
:
[
Hook
]
hookById
(
id
:
UUID
!
):
Hook
}
extend
type
Mutation
{
createHook
(
name
:
String
!
events
:
[
String
]!
url
:
String
!
includeMetadata
:
Boolean
!
includeContent
:
Boolean
!
acceptUntrusted
:
Boolean
!
authHeader
:
String
):
HookCreateResponse
updateHook
(
id
:
UUID
!
patch
:
HookUpdateInput
!
):
DefaultResponse
deleteHook
(
id
:
UUID
!
):
DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type
Hook
{
id
:
UUID
name
:
String
events
:
[
String
]
url
:
String
includeMetadata
:
Boolean
includeContent
:
Boolean
acceptUntrusted
:
Boolean
authHeader
:
String
state
:
HookState
lastErrorMessage
:
String
}
input
HookUpdateInput
{
name
:
String
events
:
[
String
]
url
:
String
includeMetadata
:
Boolean
includeContent
:
Boolean
acceptUntrusted
:
Boolean
authHeader
:
String
}
enum
HookState
{
pending
error
success
}
type
HookCreateResponse
{
operation
:
Operation
hook
:
Hook
}
server/models/hooks.js
0 → 100644
View file @
5a4a9df4
const
Model
=
require
(
'objection'
).
Model
/* global WIKI */
/**
* Hook model
*/
module
.
exports
=
class
Hook
extends
Model
{
static
get
tableName
()
{
return
'hooks'
}
static
get
jsonAttributes
()
{
return
[
'events'
]
}
$beforeUpdate
()
{
this
.
updatedAt
=
new
Date
()
}
static
async
createHook
(
data
)
{
return
WIKI
.
models
.
hooks
.
query
().
insertAndFetch
({
name
:
data
.
name
,
events
:
data
.
events
,
url
:
data
.
url
,
includeMetadata
:
data
.
includeMetadata
,
includeContent
:
data
.
includeContent
,
acceptUntrusted
:
data
.
acceptUntrusted
,
authHeader
:
data
.
authHeader
,
state
:
'pending'
,
lastErrorMessage
:
null
})
}
static
async
updateHook
(
id
,
patch
)
{
return
WIKI
.
models
.
hooks
.
query
().
findById
(
id
).
patch
({
...
patch
,
state
:
'pending'
,
lastErrorMessage
:
null
})
}
static
async
deleteHook
(
id
)
{
return
WIKI
.
models
.
hooks
.
query
().
deleteById
(
id
)
}
}
ux/src/components/WebhookDeleteDialog.vue
View file @
5a4a9df4
<
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.webhooks.delete`
)
}}
span
{{
t
(
`admin.webhooks.delete`
)
}}
q-card-section
.text-body2
i18n-t(keypath='admin.webhooks.deleteConfirm')
template(v-slot:name)
strong
{{
hook
.
name
}}
.text-body2.q-mt-md
strong.text-negative
{{
$
t
(
`admin.webhooks.deleteConfirmWarn`
)
}}
strong.text-negative
{{
t
(
`admin.webhooks.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'
:loading='state.isLoading'
)
</
template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
{
useI18n
}
from
'vue-i18n'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
reactive
}
from
'vue'
export
default
{
props
:
{
hook
:
{
type
:
Object
}
},
emits
:
[
'ok'
,
'hide'
],
data
()
{
return
{
}
},
methods
:
{
show
()
{
this
.
$refs
.
dialog
.
show
()
},
hide
()
{
this
.
$refs
.
dialog
.
hide
()
},
onDialogHide
()
{
this
.
$emit
(
'hide'
)
},
async
confirm
()
{
try
{
const
resp
=
await
this
.
$apollo
.
mutate
({
mutation
:
gql
`
mutation deleteHook ($id: UUID!) {
deleteHook(id: $id) {
status {
succeeded
message
}
}
// PROPS
const
props
=
defineProps
({
hook
:
{
type
:
Object
,
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 deleteHook ($id: UUID!) {
deleteHook(id: $id) {
operation {
succeeded
message
}
`
,
variables
:
{
id
:
this
.
hook
.
id
}
})
if
(
resp
?.
data
?.
deleteHook
?.
status
?.
succeeded
)
{
this
.
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$t
(
'admin.webhooks.deleteSuccess'
)
})
this
.
$emit
(
'ok'
)
this
.
hide
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
deleteHook
?.
status
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
`
,
variables
:
{
id
:
props
.
hook
.
id
}
})
if
(
resp
?.
data
?.
deleteHook
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.webhooks.deleteSuccess'
)
})
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
deleteHook
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
})
}
state
.
isLoading
=
false
}
</
script
>
ux/src/components/WebhookEditDialog.vue
View file @
5a4a9df4
<
template
lang=
"pug"
>
q-dialog(ref='dialog', @hide='onDialogHide')
q-dialog(ref='dialog
Ref
', @hide='onDialogHide')
q-card(style='min-width: 850px;')
q-card-section.card-header
template(v-if='hookId')
template(v-if='
props.
hookId')
q-icon(name='img:/_assets/icons/fluent-pencil-drawing.svg', left, size='sm')
span
{{
$
t
(
`admin.webhooks.edit`
)
}}
span
{{
t
(
`admin.webhooks.edit`
)
}}
template(v-else)
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
span
{{
$
t
(
`admin.webhooks.new`
)
}}
span
{{
t
(
`admin.webhooks.new`
)
}}
//- STATE INFO BAR
q-card-section.flex.items-center.bg-indigo.text-white(v-if='
hookId &&
hook.state === `pending`')
q-card-section.flex.items-center.bg-indigo.text-white(v-if='
props.hookId && state.
hook.state === `pending`')
q-spinner-clock.q-mr-sm(
color='white'
size='xs'
)
.text-caption
{{
$
t
(
'admin.webhooks.statePendingHint'
)
}}
q-card-section.flex.items-center.bg-positive.text-white(v-if='
hookId &&
hook.state === `success`')
.text-caption
{{
t
(
'admin.webhooks.statePendingHint'
)
}}
q-card-section.flex.items-center.bg-positive.text-white(v-if='
props.hookId && state.
hook.state === `success`')
q-spinner-infinity.q-mr-sm(
color='white'
size='xs'
)
.text-caption
{{
$
t
(
'admin.webhooks.stateSuccessHint'
)
}}
q-card-section.bg-negative.text-white(v-if='
hookId &&
hook.state === `error`')
.text-caption
{{
t
(
'admin.webhooks.stateSuccessHint'
)
}}
q-card-section.bg-negative.text-white(v-if='
props.hookId && state.
hook.state === `error`')
.flex.items-center
q-icon.q-mr-sm(
color='white'
size='xs'
name='las la-exclamation-triangle'
)
.text-caption
{{
$
t
(
'admin.webhooks.stateErrorExplain'
)
}}
.text-caption.q-pl-lg.q-ml-xs.text-red-2
{{
hook
.
lastErrorMessage
}}
.text-caption
{{
t
(
'admin.webhooks.stateErrorExplain'
)
}}
.text-caption.q-pl-lg.q-ml-xs.text-red-2
{{
state
.
hook
.
lastErrorMessage
}}
//- FORM
q-form.q-py-sm(ref='editWebhookForm')
q-item
...
...
@@ -37,15 +37,12 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-item-section
q-input(
outlined
v-model='hook.name'
v-model='
state.
hook.name'
dense
:rules=`[
val => val.length > 0 || $t('admin.webhooks.nameMissing'),
val => /^[^<>"]+$/.test(val) || $t('admin.webhooks.nameInvalidChars')
]`
:rules='hookNameValidation'
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
)
...
...
@@ -55,7 +52,7 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-select(
outlined
:options='events'
v-model='hook.events'
v-model='
state.
hook.events'
multiple
map-options
emit-value
...
...
@@ -63,21 +60,18 @@ q-dialog(ref='dialog', @hide='onDialogHide')
option-label='name'
options-dense
dense
:rules=`[
val => val.length > 0 || $t('admin.webhooks.eventsMissing')
]`
:rules='hookEventsValidation'
hide-bottom-space
:label='
$
t(`admin.webhooks.events`)'
:aria-label='
$
t(`admin.webhooks.events`)'
:label='t(`admin.webhooks.events`)'
:aria-label='t(`admin.webhooks.events`)'
lazy-rules='ondemand'
)
template(v-slot:selected)
.text-caption(v-if='
hook.events.length > 0')
{{
$tc
(
`admin.webhooks.eventsSelected`
,
hook
.
events
.
length
,
{
count
:
hook
.
events
.
length
}
)
}}
span
(
v
-
else
)
template
(
v
-
slot
:
option
=
'{ itemProps,
itemEvents,
opt, selected, toggleOption
}
'
)
.text-caption(v-if='
state.hook.events.length > 0')
{{
t
(
`admin.webhooks.eventsSelected`
,
state
.
hook
.
events
.
length
,
{
count
:
state
.
hook
.
events
.
length
}
)
}}
span
(
v
-
else
)
&
nbsp
;
template
(
v
-
slot
:
option
=
'{ itemProps, opt, selected, toggleOption
}
'
)
q
-
item
(
v
-
bind
=
'itemProps'
v
-
on
=
'itemEvents'
)
q
-
item
-
section
(
side
)
q
-
checkbox
(
...
...
@@ -97,19 +91,16 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q
-
item
blueprint
-
icon
.
self
-
start
(
icon
=
'unknown-status'
)
q
-
item
-
section
q
-
item
-
label
{{
$
t
(
`admin.webhooks.url`
)
}}
q
-
item
-
label
(
caption
)
{{
$
t
(
`admin.webhooks.urlHint`
)
}}
q
-
item
-
label
{{
t
(
`admin.webhooks.url`
)
}}
q
-
item
-
label
(
caption
)
{{
t
(
`admin.webhooks.urlHint`
)
}}
q
-
input
.
q
-
mt
-
sm
(
outlined
v
-
model
=
'hook.url'
v
-
model
=
'
state.
hook.url'
dense
:
rules
=
`[
val => (val.length > 0 && val.startsWith('http')) || $t('admin.webhooks.urlMissing'),
val => /^[^<>"]+$/.test(val) || $t('admin.webhooks.urlInvalidChars')
]`
:
rules
=
'hookUrlValidation'
hide
-
bottom
-
space
placeholder
=
'https://'
:
aria
-
label
=
'
$
t(`admin.webhooks.url`)'
:
aria
-
label
=
't(`admin.webhooks.url`)'
lazy
-
rules
=
'ondemand'
)
template
(
v
-
slot
:
prepend
)
...
...
@@ -122,301 +113,328 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q
-
item
(
tag
=
'label'
,
v
-
ripple
)
blueprint
-
icon
(
icon
=
'rescan-document'
)
q
-
item
-
section
q
-
item
-
label
{{
$
t
(
`admin.webhooks.includeMetadata`
)
}}
q
-
item
-
label
(
caption
)
{{
$
t
(
`admin.webhooks.includeMetadataHint`
)
}}
q
-
item
-
label
{{
t
(
`admin.webhooks.includeMetadata`
)
}}
q
-
item
-
label
(
caption
)
{{
t
(
`admin.webhooks.includeMetadataHint`
)
}}
q
-
item
-
section
(
avatar
)
q
-
toggle
(
v
-
model
=
'hook.includeMetadata'
v
-
model
=
'
state.
hook.includeMetadata'
color
=
'primary'
checked
-
icon
=
'las la-check'
unchecked
-
icon
=
'las la-times'
:
aria
-
label
=
'
$
t(`admin.webhooks.includeMetadata`)'
:
aria
-
label
=
't(`admin.webhooks.includeMetadata`)'
)
q
-
item
(
tag
=
'label'
,
v
-
ripple
)
blueprint
-
icon
(
icon
=
'select-all'
)
q
-
item
-
section
q
-
item
-
label
{{
$
t
(
`admin.webhooks.includeContent`
)
}}
q
-
item
-
label
(
caption
)
{{
$
t
(
`admin.webhooks.includeContentHint`
)
}}
q
-
item
-
label
{{
t
(
`admin.webhooks.includeContent`
)
}}
q
-
item
-
label
(
caption
)
{{
t
(
`admin.webhooks.includeContentHint`
)
}}
q
-
item
-
section
(
avatar
)
q
-
toggle
(
v
-
model
=
'hook.includeContent'
v
-
model
=
'
state.
hook.includeContent'
color
=
'primary'
checked
-
icon
=
'las la-check'
unchecked
-
icon
=
'las la-times'
:
aria
-
label
=
'
$
t(`admin.webhooks.includeContent`)'
:
aria
-
label
=
't(`admin.webhooks.includeContent`)'
)
q
-
item
(
tag
=
'label'
,
v
-
ripple
)
blueprint
-
icon
(
icon
=
'security-ssl'
)
q
-
item
-
section
q
-
item
-
label
{{
$
t
(
`admin.webhooks.acceptUntrusted`
)
}}
q
-
item
-
label
(
caption
)
{{
$
t
(
`admin.webhooks.acceptUntrustedHint`
)
}}
q
-
item
-
label
{{
t
(
`admin.webhooks.acceptUntrusted`
)
}}
q
-
item
-
label
(
caption
)
{{
t
(
`admin.webhooks.acceptUntrustedHint`
)
}}
q
-
item
-
section
(
avatar
)
q
-
toggle
(
v
-
model
=
'hook.acceptUntrusted'
v
-
model
=
'
state.
hook.acceptUntrusted'
color
=
'primary'
checked
-
icon
=
'las la-check'
unchecked
-
icon
=
'las la-times'
:
aria
-
label
=
'
$
t(`admin.webhooks.acceptUntrusted`)'
:
aria
-
label
=
't(`admin.webhooks.acceptUntrusted`)'
)
q
-
item
blueprint
-
icon
.
self
-
start
(
icon
=
'fingerprint-scan'
)
q
-
item
-
section
q
-
item
-
label
{{
$
t
(
`admin.webhooks.authHeader`
)
}}
q
-
item
-
label
(
caption
)
{{
$
t
(
`admin.webhooks.authHeaderHint`
)
}}
q
-
item
-
label
{{
t
(
`admin.webhooks.authHeader`
)
}}
q
-
item
-
label
(
caption
)
{{
t
(
`admin.webhooks.authHeaderHint`
)
}}
q
-
input
.
q
-
mt
-
sm
(
outlined
v
-
model
=
'hook.authHeader'
v
-
model
=
'
state.
hook.authHeader'
dense
:
aria
-
label
=
'
$
t(`admin.webhooks.authHeader`)'
:
aria
-
label
=
't(`admin.webhooks.authHeader`)'
)
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
(
v
-
if
=
'hookId'
v
-
if
=
'
props.
hookId'
unelevated
:
label
=
'
$
t(`common.actions.save`)'
:
label
=
't(`common.actions.save`)'
color
=
'primary'
padding
=
'xs md'
@
click
=
'save'
:
loading
=
'
l
oading'
:
loading
=
'
state.isL
oading'
)
q
-
btn
(
v
-
else
unelevated
:
label
=
'
$
t(`common.actions.create`)'
:
label
=
't(`common.actions.create`)'
color
=
'primary'
padding
=
'xs md'
@
click
=
'create'
:
loading
=
'
l
oading'
:
loading
=
'
state.isL
oading'
)
q
-
inner
-
loading
(:
showing
=
'
l
oading'
)
q
-
inner
-
loading
(:
showing
=
'
state.isL
oading'
)
q
-
spinner
(
color
=
'accent'
,
size
=
'lg'
)
<
/template
>
<
script
>
<
script
setup
>
import
gql
from
'graphql-tag'
import
cloneDeep
from
'lodash/cloneDeep'
import
{
useI18n
}
from
'vue-i18n'
import
{
useDialogPluginComponent
,
useQuasar
}
from
'quasar'
import
{
computed
,
onMounted
,
reactive
,
ref
}
from
'vue'
import
{
QSpinnerClock
,
QSpinnerInfinity
}
from
'quasar'
// PROPS
export
default
{
components
:
{
QSpinnerClock
,
QSpinnerInfinity
}
,
props
:
{
hookId
:
{
type
:
String
,
default
:
null
}
}
,
emits
:
[
'ok'
,
'hide'
],
data
()
{
return
{
hook
:
{
name
:
''
,
events
:
[],
url
:
''
,
acceptUntrusted
:
false
,
authHeader
:
''
,
includeMetadata
:
true
,
includeContent
:
false
,
state
:
'pending'
,
lastErrorMessage
:
''
}
,
loading
:
false
const
props
=
defineProps
({
hookId
:
{
type
:
String
,
default
:
null
}
}
)
// EMITS
defineEmits
([
...
useDialogPluginComponent
.
emits
])
// QUASAR
const
{
dialogRef
,
onDialogHide
,
onDialogOK
,
onDialogCancel
}
=
useDialogPluginComponent
()
const
$q
=
useQuasar
()
// I18N
const
{
t
}
=
useI18n
()
// DATA
const
state
=
reactive
({
isLoading
:
false
,
hook
:
{
name
:
''
,
events
:
[],
url
:
''
,
acceptUntrusted
:
false
,
authHeader
:
''
,
includeMetadata
:
true
,
includeContent
:
false
,
state
:
'pending'
,
lastErrorMessage
:
''
}
}
)
// COMPUTED
const
events
=
computed
(()
=>
([
{
key
:
'page:create'
,
name
:
t
(
'admin.webhooks.eventCreatePage'
),
type
:
t
(
'admin.webhooks.typePage'
)
}
,
{
key
:
'page:edit'
,
name
:
t
(
'admin.webhooks.eventEditPage'
),
type
:
t
(
'admin.webhooks.typePage'
)
}
,
{
key
:
'page:rename'
,
name
:
t
(
'admin.webhooks.eventRenamePage'
),
type
:
t
(
'admin.webhooks.typePage'
)
}
,
{
key
:
'page:delete'
,
name
:
t
(
'admin.webhooks.eventDeletePage'
),
type
:
t
(
'admin.webhooks.typePage'
)
}
,
{
key
:
'asset:upload'
,
name
:
t
(
'admin.webhooks.eventUploadAsset'
),
type
:
t
(
'admin.webhooks.typeAsset'
)
}
,
{
key
:
'asset:edit'
,
name
:
t
(
'admin.webhooks.eventEditAsset'
),
type
:
t
(
'admin.webhooks.typeAsset'
)
}
,
{
key
:
'asset:rename'
,
name
:
t
(
'admin.webhooks.eventRenameAsset'
),
type
:
t
(
'admin.webhooks.typeAsset'
)
}
,
{
key
:
'asset:delete'
,
name
:
t
(
'admin.webhooks.eventDeleteAsset'
),
type
:
t
(
'admin.webhooks.typeAsset'
)
}
,
{
key
:
'comment:new'
,
name
:
t
(
'admin.webhooks.eventNewComment'
),
type
:
t
(
'admin.webhooks.typeComment'
)
}
,
{
key
:
'comment:edit'
,
name
:
t
(
'admin.webhooks.eventEditComment'
),
type
:
t
(
'admin.webhooks.typeComment'
)
}
,
{
key
:
'comment:delete'
,
name
:
t
(
'admin.webhooks.eventDeleteComment'
),
type
:
t
(
'admin.webhooks.typeComment'
)
}
,
{
key
:
'user:join'
,
name
:
t
(
'admin.webhooks.eventUserJoin'
),
type
:
t
(
'admin.webhooks.typeUser'
)
}
,
{
key
:
'user:login'
,
name
:
t
(
'admin.webhooks.eventUserLogin'
),
type
:
t
(
'admin.webhooks.typeUser'
)
}
,
{
key
:
'user:logout'
,
name
:
t
(
'admin.webhooks.eventUserLogout'
),
type
:
t
(
'admin.webhooks.typeUser'
)
}
]))
// REFS
const
editWebhookForm
=
ref
(
null
)
// VALIDATION RULES
const
hookNameValidation
=
[
val
=>
val
.
length
>
0
||
t
(
'admin.webhooks.nameMissing'
),
val
=>
/^
[^
<>"
]
+$/
.
test
(
val
)
||
t
(
'admin.webhooks.nameInvalidChars'
)
]
const
hookEventsValidation
=
[
val
=>
val
.
length
>
0
||
t
(
'admin.webhooks.eventsMissing'
)
]
const
hookUrlValidation
=
[
val
=>
(
val
.
length
>
0
&&
val
.
startsWith
(
'http'
))
||
t
(
'admin.webhooks.urlMissing'
),
val
=>
/^
[^
<>"
]
+$/
.
test
(
val
)
||
t
(
'admin.webhooks.urlInvalidChars'
)
]
// METHODS
async
function
fetchHook
(
id
)
{
state
.
isLoading
=
true
try
{
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query getHook (
$id: UUID!
) {
hookById (
id: $id
) {
name
events
url
includeMetadata
includeContent
acceptUntrusted
authHeader
state
lastErrorMessage
}
}
`
,
fetchPolicy
:
'no-cache'
,
variables
:
{
id
}
}
)
if
(
resp
?.
data
?.
hookById
)
{
state
.
hook
=
cloneDeep
(
resp
.
data
.
hookById
)
}
else
{
throw
new
Error
(
'Failed to fetch webhook configuration.'
)
}
}
,
computed
:
{
events
()
{
return
[
{
key
:
'page:create'
,
name
:
this
.
$t
(
'admin.webhooks.eventCreatePage'
),
type
:
this
.
$t
(
'admin.webhooks.typePage'
)
}
,
{
key
:
'page:edit'
,
name
:
this
.
$t
(
'admin.webhooks.eventEditPage'
),
type
:
this
.
$t
(
'admin.webhooks.typePage'
)
}
,
{
key
:
'page:rename'
,
name
:
this
.
$t
(
'admin.webhooks.eventRenamePage'
),
type
:
this
.
$t
(
'admin.webhooks.typePage'
)
}
,
{
key
:
'page:delete'
,
name
:
this
.
$t
(
'admin.webhooks.eventDeletePage'
),
type
:
this
.
$t
(
'admin.webhooks.typePage'
)
}
,
{
key
:
'asset:upload'
,
name
:
this
.
$t
(
'admin.webhooks.eventUploadAsset'
),
type
:
this
.
$t
(
'admin.webhooks.typeAsset'
)
}
,
{
key
:
'asset:edit'
,
name
:
this
.
$t
(
'admin.webhooks.eventEditAsset'
),
type
:
this
.
$t
(
'admin.webhooks.typeAsset'
)
}
,
{
key
:
'asset:rename'
,
name
:
this
.
$t
(
'admin.webhooks.eventRenameAsset'
),
type
:
this
.
$t
(
'admin.webhooks.typeAsset'
)
}
,
{
key
:
'asset:delete'
,
name
:
this
.
$t
(
'admin.webhooks.eventDeleteAsset'
),
type
:
this
.
$t
(
'admin.webhooks.typeAsset'
)
}
,
{
key
:
'comment:new'
,
name
:
this
.
$t
(
'admin.webhooks.eventNewComment'
),
type
:
this
.
$t
(
'admin.webhooks.typeComment'
)
}
,
{
key
:
'comment:edit'
,
name
:
this
.
$t
(
'admin.webhooks.eventEditComment'
),
type
:
this
.
$t
(
'admin.webhooks.typeComment'
)
}
,
{
key
:
'comment:delete'
,
name
:
this
.
$t
(
'admin.webhooks.eventDeleteComment'
),
type
:
this
.
$t
(
'admin.webhooks.typeComment'
)
}
,
{
key
:
'user:join'
,
name
:
this
.
$t
(
'admin.webhooks.eventUserJoin'
),
type
:
this
.
$t
(
'admin.webhooks.typeUser'
)
}
,
{
key
:
'user:login'
,
name
:
this
.
$t
(
'admin.webhooks.eventUserLogin'
),
type
:
this
.
$t
(
'admin.webhooks.typeUser'
)
}
,
{
key
:
'user:logout'
,
name
:
this
.
$t
(
'admin.webhooks.eventUserLogout'
),
type
:
this
.
$t
(
'admin.webhooks.typeUser'
)
}
]
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
}
)
onDialogHide
()
}
state
.
isLoading
=
false
}
async
function
create
()
{
state
.
isLoading
=
true
try
{
const
isFormValid
=
await
editWebhookForm
.
value
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
t
(
'admin.webhooks.createInvalidData'
))
}
}
,
methods
:
{
show
()
{
this
.
$refs
.
dialog
.
show
()
if
(
this
.
hookId
)
{
this
.
fetchHook
(
this
.
hookId
)
}
}
,
hide
()
{
this
.
$refs
.
dialog
.
hide
()
}
,
onDialogHide
()
{
this
.
$emit
(
'hide'
)
}
,
async
fetchHook
(
id
)
{
this
.
loading
=
true
try
{
const
resp
=
await
this
.
$apollo
.
query
({
query
:
gql
`
query getHook (
$id: UUID!
) {
hookById (
id: $id
) {
name
events
url
includeMetadata
includeContent
acceptUntrusted
authHeader
state
lastErrorMessage
}
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation createHook (
$name: String!
$events: [String]!
$url: String!
$includeMetadata: Boolean!
$includeContent: Boolean!
$acceptUntrusted: Boolean!
$authHeader: String
) {
createHook (
name: $name
events: $events
url: $url
includeMetadata: $includeMetadata
includeContent: $includeContent
acceptUntrusted: $acceptUntrusted
authHeader: $authHeader
) {
operation {
succeeded
message
}
`
,
fetchPolicy
:
'no-cache'
,
variables
:
{
id
}
}
)
if
(
resp
?.
data
?.
hookById
)
{
this
.
hook
=
cloneDeep
(
resp
.
data
.
hookById
)
}
else
{
throw
new
Error
(
'Failed to fetch webhook configuration.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
}
)
this
.
hide
()
}
this
.
loading
=
false
}
,
async
create
()
{
this
.
loading
=
true
try
{
const
isFormValid
=
await
this
.
$refs
.
editWebhookForm
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
this
.
$t
(
'admin.webhooks.createInvalidData'
))
}
const
resp
=
await
this
.
$apollo
.
mutate
({
mutation
:
gql
`
mutation createHook (
$name: String!
$events: [String]!
$url: String!
$includeMetadata: Boolean!
$includeContent: Boolean!
$acceptUntrusted: Boolean!
$authHeader: String
) {
createHook (
name: $name
events: $events
url: $url
includeMetadata: $includeMetadata
includeContent: $includeContent
acceptUntrusted: $acceptUntrusted
authHeader: $authHeader
) {
status {
succeeded
message
}
}
}
`
,
variables
:
this
.
hook
}
)
if
(
resp
?.
data
?.
createHook
?.
status
?.
succeeded
)
{
this
.
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$t
(
'admin.webhooks.createSuccess'
)
}
)
this
.
$emit
(
'ok'
)
this
.
hide
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
createHook
?.
status
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
}
)
}
this
.
loading
=
false
}
,
async
save
()
{
this
.
loading
=
true
try
{
const
isFormValid
=
await
this
.
$refs
.
editWebhookForm
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
this
.
$t
(
'admin.webhooks.createInvalidData'
))
}
}
const
resp
=
await
this
.
$apollo
.
mutate
({
mutation
:
gql
`
mutation saveHook (
$id: UUID!
$patch: HookUpdateInput!
) {
updateHook (
id: $id
patch: $patch
) {
status {
succeeded
message
}
}
}
`
,
variables
:
{
id
:
this
.
hookId
,
patch
:
{
name
:
this
.
hook
.
name
,
events
:
this
.
hook
.
events
,
url
:
this
.
hook
.
url
,
acceptUntrusted
:
this
.
hook
.
acceptUntrusted
,
authHeader
:
this
.
hook
.
authHeader
,
includeMetadata
:
this
.
hook
.
includeMetadata
,
includeContent
:
this
.
hook
.
includeContent
`
,
variables
:
state
.
hook
}
)
if
(
resp
?.
data
?.
createHook
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.webhooks.createSuccess'
)
}
)
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
createHook
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
}
)
}
state
.
isLoading
=
false
}
async
function
save
()
{
state
.
isLoading
=
true
try
{
const
isFormValid
=
await
editWebhookForm
.
value
.
validate
(
true
)
if
(
!
isFormValid
)
{
throw
new
Error
(
t
(
'admin.webhooks.createInvalidData'
))
}
const
resp
=
await
APOLLO_CLIENT
.
mutate
({
mutation
:
gql
`
mutation saveHook (
$id: UUID!
$patch: HookUpdateInput!
) {
updateHook (
id: $id
patch: $patch
) {
operation {
succeeded
message
}
}
}
)
if
(
resp
?.
data
?.
updateHook
?.
status
?.
succeeded
)
{
this
.
$q
.
notify
({
type
:
'positive'
,
message
:
this
.
$t
(
'admin.webhooks.updateSuccess'
)
}
)
this
.
$emit
(
'ok'
)
this
.
hide
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
updateHook
?.
status
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
this
.
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
}
)
`
,
variables
:
{
id
:
props
.
hookId
,
patch
:
{
name
:
state
.
hook
.
name
,
events
:
state
.
hook
.
events
,
url
:
state
.
hook
.
url
,
acceptUntrusted
:
state
.
hook
.
acceptUntrusted
,
authHeader
:
state
.
hook
.
authHeader
,
includeMetadata
:
state
.
hook
.
includeMetadata
,
includeContent
:
state
.
hook
.
includeContent
}
}
this
.
loading
=
false
}
)
if
(
resp
?.
data
?.
updateHook
?.
operation
?.
succeeded
)
{
$q
.
notify
({
type
:
'positive'
,
message
:
t
(
'admin.webhooks.updateSuccess'
)
}
)
onDialogOK
()
}
else
{
throw
new
Error
(
resp
?.
data
?.
updateHook
?.
operation
?.
message
||
'An unexpected error occured.'
)
}
}
catch
(
err
)
{
$q
.
notify
({
type
:
'negative'
,
message
:
err
.
message
}
)
}
state
.
isLoading
=
false
}
// MOUNTED
onMounted
(()
=>
{
if
(
props
.
hookId
)
{
fetchHook
(
props
.
hookId
)
}
}
)
<
/script
>
ux/src/pages/AdminLocale.vue
View file @
5a4a9df4
...
...
@@ -41,7 +41,7 @@ q-page.admin-locale
)
q-separator(inset)
.row.q-pa-md.q-col-gutter-md
.col-7
.col-
12.col-lg-
7
//- -----------------------
//- Locale Options
//- -----------------------
...
...
@@ -89,7 +89,7 @@ q-page.admin-locale
span
{{
t
(
'admin.locale.namespacingPrefixWarning.title'
,
{
langCode
:
state
.
selectedLocale
}
)
}}
.
text
-
caption
.
text
-
yellow
-
1
{{
t
(
'admin.locale.namespacingPrefixWarning.subtitle'
)
}}
.
col
-
5
.
col
-
12
.
col
-
lg
-
5
//- -----------------------
//- Namespacing
//- -----------------------
...
...
ux/src/pages/AdminWebhooks.vue
View file @
5a4a9df4
...
...
@@ -4,14 +4,9 @@ q-page.admin-webhooks
.col-auto
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-lightning-bolt.svg')
.col.q-pl-md
.text-h5.text-primary.animated.fadeInLeft
{{
$
t
(
'admin.webhooks.title'
)
}}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s
{{
$
t
(
'admin.webhooks.subtitle'
)
}}
.text-h5.text-primary.animated.fadeInLeft
{{
t
(
'admin.webhooks.title'
)
}}
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s
{{
t
(
'admin.webhooks.subtitle'
)
}}
.col-auto
q-spinner-tail.q-mr-md(
v-show='loading'
color='accent'
size='sm'
)
q-btn.q-mr-sm.acrylic-btn(
icon='las la-question-circle'
flat
...
...
@@ -24,19 +19,19 @@ q-page.admin-webhooks
icon='las la-redo-alt'
flat
color='secondary'
:loading='loading > 0'
:loading='
state.
loading > 0'
@click='load'
)
q-btn(
unelevated
icon='las la-plus'
:label='
$
t(`admin.webhooks.new`)'
:label='t(`admin.webhooks.new`)'
color='primary'
@click='createHook'
)
q-separator(inset)
.row.q-pa-md.q-col-gutter-md
.col-12(v-if='hooks.length < 1')
.col-12(v-if='
state.
hooks.length < 1')
q-card.rounded-borders(
flat
:class='$q.dark.isActive ? `bg-dark-5 text-white` : `bg-grey-3 text-dark`'
...
...
@@ -44,11 +39,11 @@ q-page.admin-webhooks
q-card-section.items-center(horizontal)
q-card-section.col-auto.q-pr-none
q-icon(name='las la-info-circle', size='sm')
q-card-section.text-caption
{{
$
t
(
'admin.webhooks.none'
)
}}
q-card-section.text-caption
{{
t
(
'admin.webhooks.none'
)
}}
.col-12(v-else)
q-card
q-list(separator)
q-item(v-for='hook of hooks', :key='hook.id')
q-item(v-for='hook of
state.
hooks', :key='hook.id')
q-item-section(side)
q-icon(name='las la-bolt', color='primary')
q-item-section
...
...
@@ -60,23 +55,23 @@ q-page.admin-webhooks
color='indigo'
size='xs'
)
.text-caption.text-indigo
{{
$
t
(
'admin.webhooks.statePending'
)
}}
q-tooltip(anchor='center left', self='center right')
{{
$
t
(
'admin.webhooks.statePendingHint'
)
}}
.text-caption.text-indigo
{{
t
(
'admin.webhooks.statePending'
)
}}
q-tooltip(anchor='center left', self='center right')
{{
t
(
'admin.webhooks.statePendingHint'
)
}}
template(v-else-if='hook.state === `success`')
q-spinner-infinity.q-mr-sm(
color='positive'
size='xs'
)
.text-caption.text-positive
{{
$
t
(
'admin.webhooks.stateSuccess'
)
}}
q-tooltip(anchor='center left', self='center right')
{{
$
t
(
'admin.webhooks.stateSuccessHint'
)
}}
.text-caption.text-positive
{{
t
(
'admin.webhooks.stateSuccess'
)
}}
q-tooltip(anchor='center left', self='center right')
{{
t
(
'admin.webhooks.stateSuccessHint'
)
}}
template(v-else-if='hook.state === `error`')
q-icon.q-mr-sm(
color='negative'
size='xs'
name='las la-exclamation-triangle'
)
.text-caption.text-negative
{{
$
t
(
'admin.webhooks.stateError'
)
}}
q-tooltip(anchor='center left', self='center right')
{{
$
t
(
'admin.webhooks.stateErrorHint'
)
}}
.text-caption.text-negative
{{
t
(
'admin.webhooks.stateError'
)
}}
q-tooltip(anchor='center left', self='center right')
{{
t
(
'admin.webhooks.stateErrorHint'
)
}}
q-separator.q-ml-md(vertical)
q-item-section(side, style='flex-direction: row; align-items: center;')
q-btn.acrylic-btn.q-mr-sm(
...
...
@@ -96,88 +91,100 @@ q-page.admin-webhooks
</
template
>
<
script
>
<
script
setup
>
import
cloneDeep
from
'lodash/cloneDeep'
import
gql
from
'graphql-tag'
import
{
createMetaMixin
,
QSpinnerClock
,
QSpinnerInfinity
}
from
'quasar'
import
WebhookDeleteDialog
from
'../components/WebhookDeleteDialog.vue'
import
WebhookEditDialog
from
'../components/WebhookEditDialog.vue'
export
default
{
components
:
{
QSpinnerClock
,
QSpinnerInfinity
},
mixins
:
[
createMetaMixin
(
function
()
{
return
{
title
:
this
.
$t
(
'admin.webhooks.title'
)
import
{
useI18n
}
from
'vue-i18n'
import
{
useMeta
,
useQuasar
}
from
'quasar'
import
{
onMounted
,
reactive
}
from
'vue'
import
WebhookEditDialog
from
'src/components/WebhookEditDialog.vue'
import
WebhookDeleteDialog
from
'src/components/WebhookDeleteDialog.vue'
// QUASAR
const
$q
=
useQuasar
()
// I18N
const
{
t
}
=
useI18n
()
// META
useMeta
({
title
:
t
(
'admin.webhooks.title'
)
})
// DATA
const
state
=
reactive
({
hooks
:
[],
loading
:
0
})
// METHODS
async
function
load
()
{
state
.
loading
++
$q
.
loading
.
show
()
const
resp
=
await
APOLLO_CLIENT
.
query
({
query
:
gql
`
query getHooks {
hooks {
id
name
url
state
}
}
})
],
data
()
{
return
{
hooks
:
[],
loading
:
0
`
,
fetchPolicy
:
'network-only'
})
state
.
hooks
=
cloneDeep
(
resp
?.
data
?.
hooks
)
??
[]
$q
.
loading
.
hide
()
state
.
loading
--
}
function
createHook
()
{
$q
.
dialog
({
component
:
WebhookEditDialog
,
componentProps
:
{
hookId
:
null
}
},
mounted
()
{
this
.
load
()
},
methods
:
{
async
load
()
{
this
.
loading
++
this
.
$q
.
loading
.
show
()
const
resp
=
await
this
.
$apollo
.
query
({
query
:
gql
`
query getHooks {
hooks {
id
name
url
state
}
}
`
,
fetchPolicy
:
'network-only'
})
this
.
config
=
cloneDeep
(
resp
?.
data
?.
hooks
)
??
[]
this
.
$q
.
loading
.
hide
()
this
.
loading
--
},
createHook
()
{
this
.
$q
.
dialog
({
component
:
WebhookEditDialog
,
componentProps
:
{
hookId
:
null
}
}).
onOk
(()
=>
{
this
.
load
()
})
},
editHook
(
id
)
{
this
.
$q
.
dialog
({
component
:
WebhookEditDialog
,
componentProps
:
{
hookId
:
id
}
}).
onOk
(()
=>
{
this
.
load
()
})
},
deleteHook
(
hook
)
{
this
.
$q
.
dialog
({
component
:
WebhookDeleteDialog
,
componentProps
:
{
hook
}
}).
onOk
(()
=>
{
this
.
load
()
})
}).
onOk
(()
=>
{
load
()
})
}
function
editHook
(
id
)
{
$q
.
dialog
({
component
:
WebhookEditDialog
,
componentProps
:
{
hookId
:
id
}
}
}).
onOk
(()
=>
{
load
()
})
}
function
deleteHook
(
hook
)
{
$q
.
dialog
({
component
:
WebhookDeleteDialog
,
componentProps
:
{
hook
}
}).
onOk
(()
=>
{
load
()
})
}
// MOUNTED
onMounted
(()
=>
{
load
()
})
</
script
>
<
style
lang=
'scss'
>
...
...
ux/src/router/routes.js
View file @
5a4a9df4
...
...
@@ -50,7 +50,7 @@ const routes = [
{
path
:
'security'
,
component
:
()
=>
import
(
'../pages/AdminSecurity.vue'
)
},
{
path
:
'system'
,
component
:
()
=>
import
(
'../pages/AdminSystem.vue'
)
},
// { path: 'utilities', component: () => import('../pages/AdminUtilities.vue') },
//
{ path: 'webhooks', component: () => import('../pages/AdminWebhooks.vue') },
{
path
:
'webhooks'
,
component
:
()
=>
import
(
'../pages/AdminWebhooks.vue'
)
},
{
path
:
'flags'
,
component
:
()
=>
import
(
'../pages/AdminFlags.vue'
)
}
]
},
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment