mirror of
https://github.com/requarks/wiki.git
synced 2026-08-26 13:17:10 -05:00
feat(admin): migrate users to vue 3 composable
This commit is contained in:
@@ -89,7 +89,7 @@ module.exports = {
|
||||
await WIKI.models.users.createNewUser({ ...args, passwordRaw: args.password, isVerified: true })
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User created successfully')
|
||||
operation: graphHelper.generateSuccess('User created successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
@@ -106,7 +106,7 @@ module.exports = {
|
||||
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User deleted successfully')
|
||||
operation: graphHelper.generateSuccess('User deleted successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.message.indexOf('foreign') >= 0) {
|
||||
@@ -121,7 +121,7 @@ module.exports = {
|
||||
await WIKI.models.users.updateUser(args.id, args.patch)
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User updated successfully')
|
||||
operation: graphHelper.generateSuccess('User updated successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
@@ -132,7 +132,7 @@ module.exports = {
|
||||
await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id)
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User verified successfully')
|
||||
operation: graphHelper.generateSuccess('User verified successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
@@ -143,7 +143,7 @@ module.exports = {
|
||||
await WIKI.models.users.query().patch({ isActive: true }).findById(args.id)
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User activated successfully')
|
||||
operation: graphHelper.generateSuccess('User activated successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
@@ -160,7 +160,7 @@ module.exports = {
|
||||
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User deactivated successfully')
|
||||
operation: graphHelper.generateSuccess('User deactivated successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
@@ -171,7 +171,7 @@ module.exports = {
|
||||
await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User 2FA enabled successfully')
|
||||
operation: graphHelper.generateSuccess('User 2FA enabled successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
@@ -182,7 +182,7 @@ module.exports = {
|
||||
await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User 2FA disabled successfully')
|
||||
operation: graphHelper.generateSuccess('User 2FA disabled successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
@@ -225,7 +225,7 @@ module.exports = {
|
||||
const newToken = await WIKI.models.users.refreshToken(usr.id)
|
||||
|
||||
return {
|
||||
status: graphHelper.generateSuccess('User profile updated successfully'),
|
||||
operation: graphHelper.generateSuccess('User profile updated successfully'),
|
||||
jwt: newToken.token
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"uuid": "8.3.2",
|
||||
"v-network-graph": "0.5.16",
|
||||
"vue": "3.2.31",
|
||||
"vue-codemirror": "5.0.1",
|
||||
"vue-i18n": "9.1.10",
|
||||
"vue-router": "4.0.15",
|
||||
"vuedraggable": "4.1.0",
|
||||
|
||||
@@ -501,7 +501,7 @@ import { fileOpen } from 'browser-fs-access'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { exportFile, useQuasar } from 'quasar'
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue'
|
||||
import { computed, onMounted, reactive, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
import { useAdminStore } from 'src/stores/admin'
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
<template lang="pug">
|
||||
q-dialog(ref='dialog', @hide='onDialogHide')
|
||||
q-dialog(ref='dialogRef', @hide='onDialogHide')
|
||||
q-card(style='min-width: 650px;')
|
||||
q-card-section.card-header
|
||||
q-icon(name='img:/_assets/icons/fluent-password-reset.svg', left, size='sm')
|
||||
span {{$t(`admin.users.changePassword`)}}
|
||||
span {{t(`admin.users.changePassword`)}}
|
||||
q-form.q-py-sm(ref='changeUserPwdForm', @submit='save')
|
||||
q-item
|
||||
blueprint-icon(icon='password')
|
||||
q-item-section
|
||||
q-input(
|
||||
outlined
|
||||
v-model='userPassword'
|
||||
v-model='state.userPassword'
|
||||
dense
|
||||
:rules=`[
|
||||
val => val.length > 0 || $t('admin.users.passwordMissing'),
|
||||
val => val.length >= 8 || $t('admin.users.passwordTooShort')
|
||||
]`
|
||||
:rules='userPasswordValidation'
|
||||
hide-bottom-space
|
||||
:label='$t(`admin.users.password`)'
|
||||
:aria-label='$t(`admin.users.password`)'
|
||||
:label='t(`admin.users.password`)'
|
||||
:aria-label='t(`admin.users.password`)'
|
||||
lazy-rules='ondemand'
|
||||
autofocus
|
||||
)
|
||||
@@ -41,159 +38,182 @@ q-dialog(ref='dialog', @hide='onDialogHide')
|
||||
q-item(tag='label', v-ripple)
|
||||
blueprint-icon(icon='password-reset')
|
||||
q-item-section
|
||||
q-item-label {{$t(`admin.users.mustChangePwd`)}}
|
||||
q-item-label(caption) {{$t(`admin.users.mustChangePwdHint`)}}
|
||||
q-item-label {{t(`admin.users.mustChangePwd`)}}
|
||||
q-item-label(caption) {{t(`admin.users.mustChangePwdHint`)}}
|
||||
q-item-section(avatar)
|
||||
q-toggle(
|
||||
v-model='userMustChangePassword'
|
||||
v-model='state.userMustChangePassword'
|
||||
color='primary'
|
||||
checked-icon='las la-check'
|
||||
unchecked-icon='las la-times'
|
||||
:aria-label='$t(`admin.users.mustChangePwd`)'
|
||||
:aria-label='t(`admin.users.mustChangePwd`)'
|
||||
)
|
||||
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.update`)'
|
||||
:label='t(`common.actions.update`)'
|
||||
color='primary'
|
||||
padding='xs md'
|
||||
@click='save'
|
||||
:loading='isLoading'
|
||||
:loading='state.isLoading'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import gql from 'graphql-tag'
|
||||
import sampleSize from 'lodash/sampleSize'
|
||||
import zxcvbn from 'zxcvbn'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['ok', 'hide'],
|
||||
data () {
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDialogPluginComponent, useQuasar } from 'quasar'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
// PROPS
|
||||
|
||||
const props = defineProps({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// EMITS
|
||||
|
||||
defineEmits([
|
||||
...useDialogPluginComponent.emits
|
||||
])
|
||||
|
||||
// QUASAR
|
||||
|
||||
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
|
||||
const $q = useQuasar()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
userPassword: '',
|
||||
userMustChangePassword: false,
|
||||
isLoading: false
|
||||
})
|
||||
|
||||
// REFS
|
||||
|
||||
const changeUserPwdForm = ref(null)
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const passwordStrength = computed(() => {
|
||||
if (state.userPassword.length < 8) {
|
||||
return {
|
||||
userPassword: '',
|
||||
userMustChangePassword: false,
|
||||
isLoading: false
|
||||
color: 'negative',
|
||||
label: t('admin.users.pwdStrengthWeak')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
passwordStrength () {
|
||||
if (this.userPassword.length < 8) {
|
||||
} else {
|
||||
switch (zxcvbn(state.userPassword).score) {
|
||||
case 1:
|
||||
return {
|
||||
color: 'deep-orange-7',
|
||||
label: t('admin.users.pwdStrengthPoor')
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
color: 'purple-7',
|
||||
label: t('admin.users.pwdStrengthMedium')
|
||||
}
|
||||
case 3:
|
||||
return {
|
||||
color: 'blue-7',
|
||||
label: t('admin.users.pwdStrengthGood')
|
||||
}
|
||||
case 4:
|
||||
return {
|
||||
color: 'green-7',
|
||||
label: t('admin.users.pwdStrengthStrong')
|
||||
}
|
||||
default:
|
||||
return {
|
||||
color: 'negative',
|
||||
label: this.$t('admin.users.pwdStrengthWeak')
|
||||
label: t('admin.users.pwdStrengthWeak')
|
||||
}
|
||||
} else {
|
||||
switch (zxcvbn(this.userPassword).score) {
|
||||
case 1:
|
||||
return {
|
||||
color: 'deep-orange-7',
|
||||
label: this.$t('admin.users.pwdStrengthPoor')
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
color: 'purple-7',
|
||||
label: this.$t('admin.users.pwdStrengthMedium')
|
||||
}
|
||||
case 3:
|
||||
return {
|
||||
color: 'blue-7',
|
||||
label: this.$t('admin.users.pwdStrengthGood')
|
||||
}
|
||||
case 4:
|
||||
return {
|
||||
color: 'green-7',
|
||||
label: this.$t('admin.users.pwdStrengthStrong')
|
||||
}
|
||||
default:
|
||||
return {
|
||||
color: 'negative',
|
||||
label: this.$t('admin.users.pwdStrengthWeak')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show () {
|
||||
this.$refs.dialog.show()
|
||||
},
|
||||
hide () {
|
||||
this.$refs.dialog.hide()
|
||||
},
|
||||
onDialogHide () {
|
||||
this.$emit('hide')
|
||||
},
|
||||
randomizePassword () {
|
||||
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
|
||||
this.userPassword = sampleSize(pwdChars, 16).join('')
|
||||
},
|
||||
async save () {
|
||||
this.isLoading = true
|
||||
try {
|
||||
const isFormValid = await this.$refs.changeUserPwdForm.validate(true)
|
||||
if (!isFormValid) {
|
||||
throw new Error(this.$t('admin.users.createInvalidData'))
|
||||
}
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation adminUpdateUserPwd (
|
||||
$id: UUID!
|
||||
$patch: UserUpdateInput!
|
||||
) {
|
||||
updateUser (
|
||||
id: $id
|
||||
patch: $patch
|
||||
) {
|
||||
status {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: this.userId,
|
||||
patch: {
|
||||
newPassword: this.userPassword,
|
||||
mustChangePassword: this.userMustChangePassword
|
||||
}
|
||||
}
|
||||
})
|
||||
if (resp?.data?.updateUser?.status?.succeeded) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: this.$t('admin.users.createSuccess')
|
||||
})
|
||||
this.$emit('ok', {
|
||||
mustChangePassword: this.userMustChangePassword
|
||||
})
|
||||
this.hide()
|
||||
} else {
|
||||
throw new Error(resp?.data?.updateUser?.status?.message || 'An unexpected error occured.')
|
||||
}
|
||||
} catch (err) {
|
||||
this.$q.notify({
|
||||
type: 'negative',
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// VALIDATION RULES
|
||||
|
||||
const userPasswordValidation = [
|
||||
val => val.length > 0 || t('admin.users.passwordMissing'),
|
||||
val => val.length >= 8 || t('admin.users.passwordTooShort')
|
||||
]
|
||||
|
||||
// METHODS
|
||||
|
||||
function randomizePassword () {
|
||||
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
|
||||
state.userPassword = sampleSize(pwdChars, 16).join('')
|
||||
}
|
||||
|
||||
async function save () {
|
||||
state.isLoading = true
|
||||
try {
|
||||
const isFormValid = await changeUserPwdForm.value.validate(true)
|
||||
if (!isFormValid) {
|
||||
throw new Error(t('admin.users.createInvalidData'))
|
||||
}
|
||||
const resp = await APOLLO_CLIENT.mutate({
|
||||
mutation: gql`
|
||||
mutation adminUpdateUserPwd (
|
||||
$id: UUID!
|
||||
$patch: UserUpdateInput!
|
||||
) {
|
||||
updateUser (
|
||||
id: $id
|
||||
patch: $patch
|
||||
) {
|
||||
operation {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: props.userId,
|
||||
patch: {
|
||||
newPassword: state.userPassword,
|
||||
mustChangePassword: state.userMustChangePassword
|
||||
}
|
||||
}
|
||||
})
|
||||
if (resp?.data?.updateUser?.operation?.succeeded) {
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: t('admin.users.createSuccess')
|
||||
})
|
||||
onDialogOK({
|
||||
mustChangePassword: state.userMustChangePassword
|
||||
})
|
||||
} else {
|
||||
throw new Error(resp?.data?.updateUser?.operation?.message || 'An unexpected error occured.')
|
||||
}
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
state.isLoading = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
<template lang="pug">
|
||||
q-dialog(ref='dialog', @hide='onDialogHide')
|
||||
q-dialog(ref='dialogRef', @hide='onDialogHide')
|
||||
q-card(style='min-width: 650px;')
|
||||
q-card-section.card-header
|
||||
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
|
||||
span {{$t(`admin.users.create`)}}
|
||||
span {{t(`admin.users.create`)}}
|
||||
q-form.q-py-sm(ref='createUserForm', @submit='create')
|
||||
q-item
|
||||
blueprint-icon(icon='person')
|
||||
q-item-section
|
||||
q-input(
|
||||
outlined
|
||||
v-model='userName'
|
||||
v-model='state.userName'
|
||||
dense
|
||||
:rules=`[
|
||||
val => val.length > 0 || $t('admin.users.nameMissing'),
|
||||
val => /^[^<>"]+$/.test(val) || $t('admin.users.nameInvalidChars')
|
||||
]`
|
||||
:rules='userNameValidation'
|
||||
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
|
||||
ref='iptName'
|
||||
@@ -28,16 +25,13 @@ q-dialog(ref='dialog', @hide='onDialogHide')
|
||||
q-item-section
|
||||
q-input(
|
||||
outlined
|
||||
v-model='userEmail'
|
||||
v-model='state.userEmail'
|
||||
dense
|
||||
type='email'
|
||||
:rules=`[
|
||||
val => val.length > 0 || $t('admin.users.emailMissing'),
|
||||
val => /^.+\@.+\..+$/.test(val) || $t('admin.users.emailInvalid')
|
||||
]`
|
||||
:rules='userEmailValidation'
|
||||
hide-bottom-space
|
||||
:label='$t(`admin.users.email`)'
|
||||
:aria-label='$t(`admin.users.email`)'
|
||||
:label='t(`admin.users.email`)'
|
||||
:aria-label='t(`admin.users.email`)'
|
||||
lazy-rules='ondemand'
|
||||
autofocus
|
||||
)
|
||||
@@ -46,15 +40,12 @@ q-dialog(ref='dialog', @hide='onDialogHide')
|
||||
q-item-section
|
||||
q-input(
|
||||
outlined
|
||||
v-model='userPassword'
|
||||
v-model='state.userPassword'
|
||||
dense
|
||||
:rules=`[
|
||||
val => val.length > 0 || $t('admin.users.passwordMissing'),
|
||||
val => val.length >= 8 || $t('admin.users.passwordTooShort')
|
||||
]`
|
||||
:rules='userPasswordValidation'
|
||||
hide-bottom-space
|
||||
:label='$t(`admin.users.password`)'
|
||||
:aria-label='$t(`admin.users.password`)'
|
||||
:label='t(`admin.users.password`)'
|
||||
:aria-label='t(`admin.users.password`)'
|
||||
lazy-rules='ondemand'
|
||||
autofocus
|
||||
)
|
||||
@@ -79,8 +70,8 @@ q-dialog(ref='dialog', @hide='onDialogHide')
|
||||
q-item-section
|
||||
q-select(
|
||||
outlined
|
||||
:options='groups'
|
||||
v-model='userGroups'
|
||||
:options='state.groups'
|
||||
v-model='state.userGroups'
|
||||
multiple
|
||||
map-options
|
||||
emit-value
|
||||
@@ -88,29 +79,26 @@ q-dialog(ref='dialog', @hide='onDialogHide')
|
||||
option-label='name'
|
||||
options-dense
|
||||
dense
|
||||
:rules=`[
|
||||
val => val.length > 0 || $t('admin.users.groupsMissing')
|
||||
]`
|
||||
:rules='userGroupsValidation'
|
||||
hide-bottom-space
|
||||
:label='$t(`admin.users.groups`)'
|
||||
:aria-label='$t(`admin.users.groups`)'
|
||||
:label='t(`admin.users.groups`)'
|
||||
:aria-label='t(`admin.users.groups`)'
|
||||
lazy-rules='ondemand'
|
||||
:loading='loadingGroups'
|
||||
:loading='state.loadingGroups'
|
||||
)
|
||||
template(v-slot:selected)
|
||||
.text-caption(v-if='userGroups.length > 1')
|
||||
.text-caption(v-if='state.userGroups.length > 1')
|
||||
i18n-t(keypath='admin.users.groupsSelected')
|
||||
template(#count)
|
||||
strong {{ userGroups.length }}
|
||||
.text-caption(v-else-if='userGroups.length === 1')
|
||||
strong {{ state.userGroups.length }}
|
||||
.text-caption(v-else-if='state.userGroups.length === 1')
|
||||
i18n-t(keypath='admin.users.groupSelected')
|
||||
template(#group)
|
||||
strong {{ selectedGroupName }}
|
||||
span(v-else)
|
||||
template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }')
|
||||
template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
|
||||
q-item(
|
||||
v-bind='itemProps'
|
||||
v-on='itemEvents'
|
||||
)
|
||||
q-item-section(side)
|
||||
q-checkbox(
|
||||
@@ -123,214 +111,254 @@ q-dialog(ref='dialog', @hide='onDialogHide')
|
||||
q-item(tag='label', v-ripple)
|
||||
blueprint-icon(icon='password-reset')
|
||||
q-item-section
|
||||
q-item-label {{$t(`admin.users.mustChangePwd`)}}
|
||||
q-item-label(caption) {{$t(`admin.users.mustChangePwdHint`)}}
|
||||
q-item-label {{t(`admin.users.mustChangePwd`)}}
|
||||
q-item-label(caption) {{t(`admin.users.mustChangePwdHint`)}}
|
||||
q-item-section(avatar)
|
||||
q-toggle(
|
||||
v-model='userMustChangePassword'
|
||||
v-model='state.userMustChangePassword'
|
||||
color='primary'
|
||||
checked-icon='las la-check'
|
||||
unchecked-icon='las la-times'
|
||||
:aria-label='$t(`admin.users.mustChangePwd`)'
|
||||
:aria-label='t(`admin.users.mustChangePwd`)'
|
||||
)
|
||||
q-item(tag='label', v-ripple)
|
||||
blueprint-icon(icon='email-open')
|
||||
q-item-section
|
||||
q-item-label {{$t(`admin.users.sendWelcomeEmail`)}}
|
||||
q-item-label(caption) {{$t(`admin.users.sendWelcomeEmailHint`)}}
|
||||
q-item-label {{t(`admin.users.sendWelcomeEmail`)}}
|
||||
q-item-label(caption) {{t(`admin.users.sendWelcomeEmailHint`)}}
|
||||
q-item-section(avatar)
|
||||
q-toggle(
|
||||
v-model='userSendWelcomeEmail'
|
||||
v-model='state.userSendWelcomeEmail'
|
||||
color='primary'
|
||||
checked-icon='las la-check'
|
||||
unchecked-icon='las la-times'
|
||||
:aria-label='$t(`admin.users.sendWelcomeEmail`)'
|
||||
:aria-label='t(`admin.users.sendWelcomeEmail`)'
|
||||
)
|
||||
q-card-actions.card-actions
|
||||
q-checkbox(
|
||||
v-model='keepOpened'
|
||||
v-model='state.keepOpened'
|
||||
color='primary'
|
||||
:label='$t(`admin.users.createKeepOpened`)'
|
||||
:label='t(`admin.users.createKeepOpened`)'
|
||||
size='sm'
|
||||
)
|
||||
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='loading > 0'
|
||||
:loading='state.loading > 0'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import gql from 'graphql-tag'
|
||||
import sampleSize from 'lodash/sampleSize'
|
||||
import zxcvbn from 'zxcvbn'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDialogPluginComponent, useQuasar } from 'quasar'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
export default {
|
||||
emits: ['ok', 'hide'],
|
||||
data () {
|
||||
// EMITS
|
||||
|
||||
defineEmits([
|
||||
...useDialogPluginComponent.emits
|
||||
])
|
||||
|
||||
// QUASAR
|
||||
|
||||
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
|
||||
const $q = useQuasar()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
userName: '',
|
||||
userEmail: '',
|
||||
userPassword: '',
|
||||
userGroups: [],
|
||||
userMustChangePassword: false,
|
||||
userSendWelcomeEmail: false,
|
||||
keepOpened: false,
|
||||
groups: [],
|
||||
loadingGroups: false,
|
||||
loading: false
|
||||
})
|
||||
|
||||
// REFS
|
||||
|
||||
const createUserForm = ref(null)
|
||||
const iptName = ref(null)
|
||||
|
||||
// COMPUTED
|
||||
|
||||
const passwordStrength = computed(() => {
|
||||
if (state.userPassword.length < 8) {
|
||||
return {
|
||||
userName: '',
|
||||
userEmail: '',
|
||||
userPassword: '',
|
||||
userGroups: [],
|
||||
userMustChangePassword: false,
|
||||
userSendWelcomeEmail: false,
|
||||
keepOpened: false,
|
||||
groups: [],
|
||||
loadingGroups: false,
|
||||
loading: false
|
||||
color: 'negative',
|
||||
label: t('admin.users.pwdStrengthWeak')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
passwordStrength () {
|
||||
if (this.userPassword.length < 8) {
|
||||
} else {
|
||||
switch (zxcvbn(state.userPassword).score) {
|
||||
case 1:
|
||||
return {
|
||||
color: 'deep-orange-7',
|
||||
label: t('admin.users.pwdStrengthPoor')
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
color: 'purple-7',
|
||||
label: t('admin.users.pwdStrengthMedium')
|
||||
}
|
||||
case 3:
|
||||
return {
|
||||
color: 'blue-7',
|
||||
label: t('admin.users.pwdStrengthGood')
|
||||
}
|
||||
case 4:
|
||||
return {
|
||||
color: 'green-7',
|
||||
label: t('admin.users.pwdStrengthStrong')
|
||||
}
|
||||
default:
|
||||
return {
|
||||
color: 'negative',
|
||||
label: this.$t('admin.users.pwdStrengthWeak')
|
||||
label: t('admin.users.pwdStrengthWeak')
|
||||
}
|
||||
} else {
|
||||
switch (zxcvbn(this.userPassword).score) {
|
||||
case 1:
|
||||
return {
|
||||
color: 'deep-orange-7',
|
||||
label: this.$t('admin.users.pwdStrengthPoor')
|
||||
}
|
||||
case 2:
|
||||
return {
|
||||
color: 'purple-7',
|
||||
label: this.$t('admin.users.pwdStrengthMedium')
|
||||
}
|
||||
case 3:
|
||||
return {
|
||||
color: 'blue-7',
|
||||
label: this.$t('admin.users.pwdStrengthGood')
|
||||
}
|
||||
case 4:
|
||||
return {
|
||||
color: 'green-7',
|
||||
label: this.$t('admin.users.pwdStrengthStrong')
|
||||
}
|
||||
default:
|
||||
return {
|
||||
color: 'negative',
|
||||
label: this.$t('admin.users.pwdStrengthWeak')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedGroupName () {
|
||||
return this.groups.filter(g => g.id === this.userGroups[0])[0]?.name
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async show () {
|
||||
this.$refs.dialog.show()
|
||||
|
||||
this.loading++
|
||||
this.loadingGroups = true
|
||||
const resp = await this.$apollo.query({
|
||||
query: gql`
|
||||
query getGroupsForCreateUser {
|
||||
groups {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
this.groups = cloneDeep(resp?.data?.groups?.filter(g => g.id !== '10000000-0000-4000-0000-000000000001') ?? [])
|
||||
this.loadingGroups = false
|
||||
this.loading--
|
||||
},
|
||||
hide () {
|
||||
this.$refs.dialog.hide()
|
||||
},
|
||||
onDialogHide () {
|
||||
this.$emit('hide')
|
||||
},
|
||||
randomizePassword () {
|
||||
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
|
||||
this.userPassword = sampleSize(pwdChars, 16).join('')
|
||||
},
|
||||
async create () {
|
||||
this.loading++
|
||||
try {
|
||||
const isFormValid = await this.$refs.createUserForm.validate(true)
|
||||
if (!isFormValid) {
|
||||
throw new Error(this.$t('admin.users.createInvalidData'))
|
||||
}
|
||||
const resp = await this.$apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation createUser (
|
||||
$name: String!
|
||||
$email: String!
|
||||
$password: String!
|
||||
$groups: [UUID]!
|
||||
$mustChangePassword: Boolean!
|
||||
$sendWelcomeEmail: Boolean!
|
||||
) {
|
||||
createUser (
|
||||
name: $name
|
||||
email: $email
|
||||
password: $password
|
||||
groups: $groups
|
||||
mustChangePassword: $mustChangePassword
|
||||
sendWelcomeEmail: $sendWelcomeEmail
|
||||
) {
|
||||
status {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
name: this.userName,
|
||||
email: this.userEmail,
|
||||
password: this.userPassword,
|
||||
groups: this.userGroups,
|
||||
mustChangePassword: this.userMustChangePassword,
|
||||
sendWelcomeEmail: this.userSendWelcomeEmail
|
||||
}
|
||||
})
|
||||
if (resp?.data?.createUser?.status?.succeeded) {
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: this.$t('admin.users.createSuccess')
|
||||
})
|
||||
if (this.keepOpened) {
|
||||
this.userName = ''
|
||||
this.userEmail = ''
|
||||
this.userPassword = ''
|
||||
this.$refs.iptName.focus()
|
||||
} else {
|
||||
this.$emit('ok')
|
||||
this.hide()
|
||||
}
|
||||
} else {
|
||||
throw new Error(resp?.data?.createUser?.status?.message || 'An unexpected error occured.')
|
||||
}
|
||||
} catch (err) {
|
||||
this.$q.notify({
|
||||
type: 'negative',
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
this.loading--
|
||||
}
|
||||
}
|
||||
})
|
||||
const selectedGroupName = computed(() => {
|
||||
return state.groups.filter(g => g.id === state.userGroups[0])[0]?.name
|
||||
})
|
||||
|
||||
// VALIDATION RULES
|
||||
|
||||
const userNameValidation = [
|
||||
val => val.length > 0 || t('admin.users.nameMissing'),
|
||||
val => /^[^<>"]+$/.test(val) || t('admin.users.nameInvalidChars')
|
||||
]
|
||||
|
||||
const userEmailValidation = [
|
||||
val => val.length > 0 || t('admin.users.emailMissing'),
|
||||
val => /^.+@.+\..+$/.test(val) || t('admin.users.emailInvalid')
|
||||
]
|
||||
|
||||
const userPasswordValidation = [
|
||||
val => val.length > 0 || t('admin.users.passwordMissing'),
|
||||
val => val.length >= 8 || t('admin.users.passwordTooShort')
|
||||
]
|
||||
|
||||
const userGroupsValidation = [
|
||||
val => val.length > 0 || t('admin.users.groupsMissing')
|
||||
]
|
||||
|
||||
// METHODS
|
||||
|
||||
async function loadGroups () {
|
||||
state.loading++
|
||||
state.loadingGroups = true
|
||||
const resp = await APOLLO_CLIENT.query({
|
||||
query: gql`
|
||||
query getGroupsForCreateUser {
|
||||
groups {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
state.groups = cloneDeep(resp?.data?.groups?.filter(g => g.id !== '10000000-0000-4000-8000-000000000001') ?? [])
|
||||
state.loadingGroups = false
|
||||
state.loading--
|
||||
}
|
||||
|
||||
function randomizePassword () {
|
||||
const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
|
||||
state.userPassword = sampleSize(pwdChars, 16).join('')
|
||||
}
|
||||
|
||||
async function create () {
|
||||
state.loading++
|
||||
try {
|
||||
const isFormValid = await createUserForm.value.validate(true)
|
||||
if (!isFormValid) {
|
||||
throw new Error(t('admin.users.createInvalidData'))
|
||||
}
|
||||
const resp = await APOLLO_CLIENT.mutate({
|
||||
mutation: gql`
|
||||
mutation createUser (
|
||||
$name: String!
|
||||
$email: String!
|
||||
$password: String!
|
||||
$groups: [UUID]!
|
||||
$mustChangePassword: Boolean!
|
||||
$sendWelcomeEmail: Boolean!
|
||||
) {
|
||||
createUser (
|
||||
name: $name
|
||||
email: $email
|
||||
password: $password
|
||||
groups: $groups
|
||||
mustChangePassword: $mustChangePassword
|
||||
sendWelcomeEmail: $sendWelcomeEmail
|
||||
) {
|
||||
operation {
|
||||
succeeded
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
name: state.userName,
|
||||
email: state.userEmail,
|
||||
password: state.userPassword,
|
||||
groups: state.userGroups,
|
||||
mustChangePassword: state.userMustChangePassword,
|
||||
sendWelcomeEmail: state.userSendWelcomeEmail
|
||||
}
|
||||
})
|
||||
if (resp?.data?.createUser?.operation?.succeeded) {
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: t('admin.users.createSuccess')
|
||||
})
|
||||
if (state.keepOpened) {
|
||||
state.userName = ''
|
||||
state.userEmail = ''
|
||||
state.userPassword = ''
|
||||
iptName.value.focus()
|
||||
} else {
|
||||
onDialogOK()
|
||||
}
|
||||
} else {
|
||||
throw new Error(resp?.data?.createUser?.operation?.message || 'An unexpected error occured.')
|
||||
}
|
||||
} catch (err) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
state.loading--
|
||||
}
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(loadGroups)
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -168,6 +168,8 @@ const headers = [
|
||||
}
|
||||
]
|
||||
|
||||
// WATCHERS
|
||||
|
||||
watch(() => adminStore.overlay, (newValue, oldValue) => {
|
||||
if (newValue === '' && oldValue === 'GroupEditOverlay') {
|
||||
router.push('/_admin/groups')
|
||||
@@ -175,9 +177,7 @@ watch(() => adminStore.overlay, (newValue, oldValue) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => route.params.id, () => {
|
||||
checkOverlay()
|
||||
})
|
||||
watch(() => route.params.id, checkOverlay)
|
||||
|
||||
// METHODS
|
||||
|
||||
|
||||
+168
-136
@@ -4,12 +4,12 @@ q-page.admin-groups
|
||||
.col-auto
|
||||
img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-account.svg')
|
||||
.col.q-pl-md
|
||||
.text-h5.text-primary.animated.fadeInLeft {{ $t('admin.users.title') }}
|
||||
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ $t('admin.users.subtitle') }}
|
||||
.text-h5.text-primary.animated.fadeInLeft {{ t('admin.users.title') }}
|
||||
.text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.users.subtitle') }}
|
||||
.col-auto.flex.items-center
|
||||
q-input.denser.q-mr-sm(
|
||||
outlined
|
||||
v-model='search'
|
||||
v-model='state.search'
|
||||
dense
|
||||
:class='$q.dark.isActive ? `bg-dark` : `bg-white`'
|
||||
)
|
||||
@@ -28,29 +28,29 @@ q-page.admin-groups
|
||||
flat
|
||||
color='secondary'
|
||||
@click='load'
|
||||
:loading='loading > 0'
|
||||
:loading='state.loading > 0'
|
||||
)
|
||||
q-btn(
|
||||
unelevated
|
||||
icon='las la-plus'
|
||||
:label='$t(`admin.users.create`)'
|
||||
:label='t(`admin.users.create`)'
|
||||
color='primary'
|
||||
@click='createUser'
|
||||
:disabled='loading > 0'
|
||||
:disabled='state.loading > 0'
|
||||
)
|
||||
q-separator(inset)
|
||||
.row.q-pa-md.q-col-gutter-md
|
||||
.col-12
|
||||
q-card.shadow-1
|
||||
q-table(
|
||||
:rows='users'
|
||||
:rows='state.users'
|
||||
:columns='headers'
|
||||
row-key='id'
|
||||
flat
|
||||
hide-header
|
||||
hide-bottom
|
||||
:rows-per-page-options='[0]'
|
||||
:loading='loading > 0'
|
||||
:loading='state.loading > 0'
|
||||
)
|
||||
template(v-slot:body-cell-id='props')
|
||||
q-td(:props='props')
|
||||
@@ -92,7 +92,7 @@ q-page.admin-groups
|
||||
: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(
|
||||
@@ -100,146 +100,178 @@ q-page.admin-groups
|
||||
flat
|
||||
icon='las la-trash'
|
||||
color='accent'
|
||||
@click='deleteGroup(props.row)'
|
||||
@click='deleteUser(props.row)'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import gql from 'graphql-tag'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { DateTime } from 'luxon'
|
||||
import { sync } from 'vuex-pathify'
|
||||
import { createMetaMixin } from 'quasar'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMeta, useQuasar } from 'quasar'
|
||||
import { onBeforeUnmount, onMounted, reactive, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
import { useAdminStore } from 'src/stores/admin'
|
||||
|
||||
import UserCreateDialog from '../components/UserCreateDialog.vue'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
createMetaMixin(function () {
|
||||
return {
|
||||
title: this.$t('admin.users.title')
|
||||
// QUASAR
|
||||
|
||||
const $q = useQuasar()
|
||||
|
||||
// STORES
|
||||
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
// ROUTER
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// I18N
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// META
|
||||
|
||||
useMeta({
|
||||
title: t('admin.users.title')
|
||||
})
|
||||
|
||||
// DATA
|
||||
|
||||
const state = reactive({
|
||||
users: [],
|
||||
loading: 0,
|
||||
search: ''
|
||||
})
|
||||
|
||||
const headers = [
|
||||
{
|
||||
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'
|
||||
}
|
||||
]
|
||||
|
||||
// WATCHERS
|
||||
|
||||
watch(() => adminStore.overlay, (newValue, oldValue) => {
|
||||
if (newValue === '' && oldValue === 'UserEditOverlay') {
|
||||
router.push('/_admin/users')
|
||||
load()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => route.params.id, checkOverlay)
|
||||
|
||||
// METHODS
|
||||
|
||||
async function load () {
|
||||
state.loading++
|
||||
$q.loading.show()
|
||||
const resp = await APOLLO_CLIENT.query({
|
||||
query: gql`
|
||||
query getUsers {
|
||||
users {
|
||||
id
|
||||
name
|
||||
email
|
||||
isSystem
|
||||
isActive
|
||||
createdAt
|
||||
lastLoginAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
state.users = cloneDeep(resp?.data?.users)
|
||||
$q.loading.hide()
|
||||
state.loading--
|
||||
}
|
||||
|
||||
function humanizeDate (val) {
|
||||
return DateTime.fromISO(val).toRelative()
|
||||
}
|
||||
|
||||
function checkOverlay () {
|
||||
if (route.params?.id) {
|
||||
adminStore.$patch({
|
||||
overlayOpts: { id: route.params.id },
|
||||
overlay: 'UserEditOverlay'
|
||||
})
|
||||
} else {
|
||||
adminStore.$patch({
|
||||
overlay: ''
|
||||
})
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
users: [],
|
||||
loading: 0,
|
||||
search: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
overlay: sync('admin/overlay', false),
|
||||
headers () {
|
||||
return [
|
||||
{
|
||||
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'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
overlay (newValue, oldValue) {
|
||||
if (newValue === '' && oldValue === 'UserEditOverlay') {
|
||||
this.$router.push('/_admin/users')
|
||||
this.load()
|
||||
}
|
||||
},
|
||||
$route: 'checkOverlay'
|
||||
},
|
||||
mounted () {
|
||||
this.checkOverlay()
|
||||
this.load()
|
||||
},
|
||||
beforeUnmount () {
|
||||
this.overlay = ''
|
||||
},
|
||||
methods: {
|
||||
async load () {
|
||||
this.loading++
|
||||
this.$q.loading.show()
|
||||
const resp = await this.$apollo.query({
|
||||
query: gql`
|
||||
query getUsers {
|
||||
users {
|
||||
id
|
||||
name
|
||||
email
|
||||
isSystem
|
||||
isActive
|
||||
createdAt
|
||||
lastLoginAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
this.users = cloneDeep(resp?.data?.users)
|
||||
this.$q.loading.hide()
|
||||
this.loading--
|
||||
},
|
||||
humanizeDate (val) {
|
||||
return DateTime.fromISO(val).toRelative()
|
||||
},
|
||||
checkOverlay () {
|
||||
if (this.$route.params && this.$route.params.id) {
|
||||
this.$store.set('admin/overlayOpts', { id: this.$route.params.id })
|
||||
this.$store.set('admin/overlay', 'UserEditOverlay')
|
||||
} else {
|
||||
this.$store.set('admin/overlay', '')
|
||||
}
|
||||
},
|
||||
createUser () {
|
||||
this.$q.dialog({
|
||||
component: UserCreateDialog
|
||||
}).onOk(() => {
|
||||
this.load()
|
||||
})
|
||||
},
|
||||
deleteUser (gr) {
|
||||
this.$q.dialog({
|
||||
// component: UserDeleteDialog,
|
||||
componentProps: {
|
||||
group: gr
|
||||
}
|
||||
}).onOk(() => {
|
||||
this.load()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createUser () {
|
||||
$q.dialog({
|
||||
component: UserCreateDialog
|
||||
}).onOk(() => {
|
||||
this.load()
|
||||
})
|
||||
}
|
||||
|
||||
function deleteUser (usr) {
|
||||
$q.dialog({
|
||||
// component: UserDeleteDialog,
|
||||
componentProps: {
|
||||
user: usr
|
||||
}
|
||||
}).onOk(load)
|
||||
}
|
||||
|
||||
// MOUNTED
|
||||
|
||||
onMounted(() => {
|
||||
checkOverlay()
|
||||
load()
|
||||
})
|
||||
|
||||
// BEFORE UNMOUNT
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
adminStore.$patch({
|
||||
overlay: ''
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang='scss'>
|
||||
|
||||
@@ -42,7 +42,7 @@ const routes = [
|
||||
// -> Users
|
||||
// { path: 'auth', component: () => import('../pages/AdminAuth.vue') },
|
||||
{ path: 'groups/:id?/:section?', component: () => import('../pages/AdminGroups.vue') },
|
||||
// { path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
|
||||
{ path: 'users/:id?/:section?', component: () => import('../pages/AdminUsers.vue') },
|
||||
// -> System
|
||||
// { path: 'api', component: () => import('../pages/AdminApi.vue') },
|
||||
{ path: 'extensions', component: () => import('../pages/AdminExtensions.vue') },
|
||||
|
||||
+18
-1
@@ -97,7 +97,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@codemirror/basic-setup@npm:0.20.0":
|
||||
"@codemirror/basic-setup@npm:0.20.0, @codemirror/basic-setup@npm:^0.20.0":
|
||||
version: 0.20.0
|
||||
resolution: "@codemirror/basic-setup@npm:0.20.0"
|
||||
dependencies:
|
||||
@@ -6717,6 +6717,7 @@ __metadata:
|
||||
uuid: 8.3.2
|
||||
v-network-graph: 0.5.16
|
||||
vue: 3.2.31
|
||||
vue-codemirror: 5.0.1
|
||||
vue-i18n: 9.1.10
|
||||
vue-router: 4.0.15
|
||||
vuedraggable: 4.1.0
|
||||
@@ -6816,6 +6817,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vue-codemirror@npm:5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "vue-codemirror@npm:5.0.1"
|
||||
dependencies:
|
||||
"@codemirror/basic-setup": ^0.20.0
|
||||
"@codemirror/commands": ^0.20.0
|
||||
"@codemirror/language": ^0.20.0
|
||||
"@codemirror/state": ^0.20.0
|
||||
"@codemirror/view": ^0.20.0
|
||||
csstype: ^2.6.8
|
||||
peerDependencies:
|
||||
vue: 3.x
|
||||
checksum: 5d96312123d109e619ecec56e8ddb1b2bdf294738a1ac796d6d35deefba9bfa25e1a311a29aa79315ba2ef48c0a6df33597a8e062f64b93166801dbed632f599
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vue-demi@npm:*":
|
||||
version: 0.12.5
|
||||
resolution: "vue-demi@npm:0.12.5"
|
||||
|
||||
Reference in New Issue
Block a user