feat(admin): migrate groups dialogs to vue 3 composable

pull/5698/head
Nicolas Giard 2 years ago
parent 7e344fc6fa
commit 6e303ac648
No known key found for this signature in database
GPG Key ID: 85061B8F9D55B7C8

@ -131,9 +131,9 @@ groups:
- 'read:assets' - 'read:assets'
- 'read:comments' - 'read:comments'
- 'write:comments' - 'write:comments'
defaultPageRules: defaultRules:
- id: default - name: Default Rule
deny: false mode: ALLOW
match: START match: START
roles: roles:
- 'read:pages' - 'read:pages'
@ -142,6 +142,7 @@ groups:
- 'write:comments' - 'write:comments'
path: '' path: ''
locales: [] locales: []
sites: []
reservedPaths: reservedPaths:
- login - login
- logout - logout

@ -1,7 +1,7 @@
const graphHelper = require('../../helpers/graph') const graphHelper = require('../../helpers/graph')
const safeRegex = require('safe-regex') const safeRegex = require('safe-regex')
const _ = require('lodash') const _ = require('lodash')
const gql = require('graphql') const { v4: uuid } = require('uuid')
/* global WIKI */ /* global WIKI */
@ -30,13 +30,13 @@ module.exports = {
async assignUserToGroup (obj, args, { req }) { async assignUserToGroup (obj, args, { req }) {
// Check for guest user // Check for guest user
if (args.userId === 2) { if (args.userId === 2) {
throw new gql.GraphQLError('Cannot assign the Guest user to a group.') throw new Error('Cannot assign the Guest user to a group.')
} }
// Check for valid group // Check for valid group
const grp = await WIKI.models.groups.query().findById(args.groupId) const grp = await WIKI.models.groups.query().findById(args.groupId)
if (!grp) { if (!grp) {
throw new gql.GraphQLError('Invalid Group ID') throw new Error('Invalid Group ID')
} }
// Check assigned permissions for write:groups // Check assigned permissions for write:groups
@ -47,13 +47,13 @@ module.exports = {
return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType) return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)
}) })
) { ) {
throw new gql.GraphQLError('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 // Check for valid user
const usr = await WIKI.models.users.query().findById(args.userId) const usr = await WIKI.models.users.query().findById(args.userId)
if (!usr) { if (!usr) {
throw new gql.GraphQLError('Invalid User ID') throw new Error('Invalid User ID')
} }
// Check for existing relation // Check for existing relation
@ -62,7 +62,7 @@ module.exports = {
groupId: args.groupId groupId: args.groupId
}).first() }).first()
if (relExist) { if (relExist) {
throw new gql.GraphQLError('User is already assigned to group.') throw new Error('User is already assigned to group.')
} }
// Assign user to group // Assign user to group
@ -73,7 +73,7 @@ module.exports = {
WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })
return { 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({ const group = await WIKI.models.groups.query().insertAndFetch({
name: args.name, name: args.name,
permissions: JSON.stringify(WIKI.data.groups.defaultPermissions), 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 isSystem: false
}) })
await WIKI.auth.reloadGroups() await WIKI.auth.reloadGroups()
WIKI.events.outbound.emit('reloadGroups') WIKI.events.outbound.emit('reloadGroups')
return { return {
responseResult: graphHelper.generateSuccess('Group created successfully.'), operation: graphHelper.generateSuccess('Group created successfully.'),
group group
} }
}, },
@ -98,7 +101,7 @@ module.exports = {
*/ */
async deleteGroup (obj, args) { async deleteGroup (obj, args) {
if (args.id === 1 || args.id === 2) { if (args.id === 1 || args.id === 2) {
throw new gql.GraphQLError('Cannot delete this group.') throw new Error('Cannot delete this group.')
} }
await WIKI.models.groups.query().deleteById(args.id) await WIKI.models.groups.query().deleteById(args.id)
@ -110,7 +113,7 @@ module.exports = {
WIKI.events.outbound.emit('reloadGroups') WIKI.events.outbound.emit('reloadGroups')
return { 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) { async unassignUserFromGroup (obj, args) {
if (args.userId === 2) { if (args.userId === 2) {
throw new gql.GraphQLError('Cannot unassign Guest user') throw new Error('Cannot unassign Guest user')
} }
if (args.userId === 1 && args.groupId === 1) { if (args.userId === 1 && args.groupId === 1) {
throw new gql.GraphQLError('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) const grp = await WIKI.models.groups.query().findById(args.groupId)
if (!grp) { if (!grp) {
throw new gql.GraphQLError('Invalid Group ID') throw new Error('Invalid Group ID')
} }
const usr = await WIKI.models.users.query().findById(args.userId) const usr = await WIKI.models.users.query().findById(args.userId)
if (!usr) { if (!usr) {
throw new gql.GraphQLError('Invalid User ID') throw new Error('Invalid User ID')
} }
await grp.$relatedQuery('users').unrelate().where('userId', usr.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' }) WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })
return { 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 => { if (_.some(args.pageRules, pr => {
return pr.match === 'REGEX' && !safeRegex(pr.path) return pr.match === 'REGEX' && !safeRegex(pr.path)
})) { })) {
throw new gql.GraphQLError('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 // Set default redirect on login value
@ -164,7 +167,7 @@ module.exports = {
return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType) return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)
}) })
) { ) {
throw new gql.GraphQLError('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 // Check assigned permissions for manage:groups
@ -172,7 +175,7 @@ module.exports = {
WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) && WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) &&
args.permissions.some(p => _.last(p.split(':')) === 'system') args.permissions.some(p => _.last(p.split(':')) === 'system')
) { ) {
throw new gql.GraphQLError('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 // Update group
@ -192,7 +195,7 @@ module.exports = {
WIKI.events.outbound.emit('reloadGroups') WIKI.events.outbound.emit('reloadGroups')
return { return {
responseResult: graphHelper.generateSuccess('Group has been updated.') operation: graphHelper.generateSuccess('Group has been updated.')
} }
} }
}, },

@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"jsx": "preserve",
"paths": { "paths": {
"src/*": [ "src/*": [
"src/*" "src/*"
@ -36,4 +37,4 @@
".quasar", ".quasar",
"node_modules" "node_modules"
] ]
} }

@ -1,24 +1,21 @@
<template lang="pug"> <template lang="pug">
q-dialog(ref='dialog', @hide='onDialogHide') q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 450px;') q-card(style='min-width: 450px;')
q-card-section.card-header q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm') 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-form.q-py-sm(ref='createGroupForm', @submit='create')
q-item q-item
blueprint-icon(icon='team') blueprint-icon(icon='team')
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='groupName' v-model='state.groupName'
dense dense
:rules=`[ :rules='groupNameValidation'
val => val.length > 0 || $t('admin.groups.nameMissing'),
val => /^[^<>"]+$/.test(val) || $t('admin.groups.nameInvalidChars')
]`
hide-bottom-space hide-bottom-space
:label='$t(`common.field.name`)' :label='t(`common.field.name`)'
:aria-label='$t(`common.field.name`)' :aria-label='t(`common.field.name`)'
lazy-rules='ondemand' lazy-rules='ondemand'
autofocus autofocus
) )
@ -26,86 +23,103 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
flat flat
:label='$t(`common.actions.cancel`)' :label='t(`common.actions.cancel`)'
color='grey' color='grey'
padding='xs md' padding='xs md'
@click='hide' @click='onDialogCancel'
) )
q-btn( q-btn(
unelevated unelevated
:label='$t(`common.actions.create`)' :label='t(`common.actions.create`)'
color='primary' color='primary'
padding='xs md' padding='xs md'
@click='create' @click='create'
:loading='isLoading' :loading='state.isLoading'
) )
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive, ref } from 'vue'
export default { // EMITS
emits: ['ok', 'hide'],
data () { defineEmits([
return { ...useDialogPluginComponent.emits
groupName: '', ])
isLoading: false
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
groupName: '',
isLoading: false
})
// 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 createGroupForm.value.validate(true)
if (!isFormValid) {
throw new Error(t('admin.groups.createInvalidData'))
} }
}, const resp = await APOLLO_CLIENT.mutate({
methods: { mutation: gql`
show () { mutation createGroup (
this.$refs.dialog.show() $name: String!
}, ) {
hide () { createGroup(
this.$refs.dialog.hide() name: $name
}, ) {
onDialogHide () { operation {
this.$emit('hide') succeeded
}, message
async create () {
this.isLoading = true
try {
const isFormValid = await this.$refs.createGroupForm.validate(true)
if (!isFormValid) {
throw new Error(this.$t('admin.groups.createInvalidData'))
}
const resp = await this.$apollo.mutate({
mutation: gql`
mutation createGroup (
$name: String!
) {
createGroup(
name: $name
) {
status {
succeeded
message
}
}
} }
`,
variables: {
name: this.groupName
} }
})
if (resp?.data?.createGroup?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.groups.createSuccess')
})
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.createGroup?.status?.message || 'An unexpected error occured.')
} }
} catch (err) { `,
this.$q.notify({ variables: {
type: 'negative', name: state.groupName
message: err.message
})
} }
this.isLoading = false })
if (resp?.data?.createGroup?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.groups.createSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.createGroup?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
} }
state.isLoading = false
} }
</script> </script>

@ -1,93 +1,96 @@
<template lang="pug"> <template lang="pug">
q-dialog(ref='dialog', @hide='onDialogHide') q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;') q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm') 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 q-card-section
.text-body2 .text-body2
i18n-t(keypath='admin.groups.deleteConfirm') i18n-t(keypath='admin.groups.deleteConfirm')
template(#groupName) template(#groupName)
strong {{group.name}} strong {{props.group.name}}
.text-body2.q-mt-md .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-card-actions.card-actions
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
flat flat
:label='$t(`common.actions.cancel`)' :label='t(`common.actions.cancel`)'
color='grey' color='grey'
padding='xs md' padding='xs md'
@click='hide' @click='onDialogCancel'
) )
q-btn( q-btn(
unelevated unelevated
:label='$t(`common.actions.delete`)' :label='t(`common.actions.delete`)'
color='negative' color='negative'
padding='xs md' padding='xs md'
@click='confirm' @click='confirm'
) )
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
export default { // PROPS
props: {
group: { const props = defineProps({
type: Object, group: {
required: true type: Object,
} required: true
}, }
emits: ['ok', 'hide'], })
data () {
return { // EMITS
}
}, defineEmits([
methods: { ...useDialogPluginComponent.emits
show () { ])
this.$refs.dialog.show()
}, // QUASAR
hide () {
this.$refs.dialog.hide() const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
}, const $q = useQuasar()
onDialogHide () {
this.$emit('hide') // I18N
},
async confirm () { const { t } = useI18n()
try {
const resp = await this.$apollo.mutate({ // METHODS
mutation: gql`
mutation deleteGroup ($id: UUID!) { async function confirm () {
deleteGroup(id: $id) { try {
status { const resp = await APOLLO_CLIENT.mutate({
succeeded mutation: gql`
message mutation deleteGroup ($id: UUID!) {
} deleteGroup(id: $id) {
} operation {
succeeded
message
} }
`,
variables: {
id: this.group.id
} }
})
if (resp?.data?.deleteGroup?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.groups.deleteSuccess')
})
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.deleteGroup?.status?.message || 'An unexpected error occured.')
} }
} catch (err) { `,
this.$q.notify({ variables: {
type: 'negative', id: props.group.id
message: err.message
})
} }
})
if (resp?.data?.deleteGroup?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.groups.deleteSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.deleteGroup?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
} }
} }
</script> </script>

File diff suppressed because it is too large Load Diff

@ -12,10 +12,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
outlined outlined
v-model='state.siteName' v-model='state.siteName'
dense dense
:rules=`[ :rules='siteNameValidation'
val => val.length > 0 || t('admin.sites.nameMissing'),
val => /^[^<>"]+$/.test(val) || t('admin.sites.nameInvalidChars')
]`
hide-bottom-space hide-bottom-space
:label='t(`common.field.name`)' :label='t(`common.field.name`)'
:aria-label='t(`common.field.name`)' :aria-label='t(`common.field.name`)'
@ -29,10 +26,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
outlined outlined
v-model='state.siteHostname' v-model='state.siteHostname'
dense dense
:rules=`[ :rules='siteHostnameValidation'
val => val.length > 0 || t('admin.sites.hostnameMissing'),
val => /^(\\*)|([a-z0-9\-.:]+)$/.test(val) || t('admin.sites.hostnameInvalidChars')
]`
:hint='t(`admin.sites.hostnameHint`)' :hint='t(`admin.sites.hostnameHint`)'
hide-bottom-space hide-bottom-space
:label='t(`admin.sites.hostname`)' :label='t(`admin.sites.hostname`)'
@ -97,6 +91,17 @@ const state = reactive({
const createSiteForm = ref(null) 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 // METHODS
async function create () { async function create () {

@ -81,7 +81,7 @@ async function confirm () {
mutation: gql` mutation: gql`
mutation deleteSite ($id: UUID!) { mutation deleteSite ($id: UUID!) {
deleteSite(id: $id) { deleteSite(id: $id) {
status { operation {
succeeded succeeded
message message
} }
@ -92,7 +92,7 @@ async function confirm () {
id: props.site.id id: props.site.id
} }
}) })
if (resp?.data?.deleteSite?.status?.succeeded) { if (resp?.data?.deleteSite?.operation?.succeeded) {
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
message: t('admin.sites.deleteSuccess') message: t('admin.sites.deleteSuccess')
@ -102,7 +102,7 @@ async function confirm () {
}) })
onDialogOK() onDialogOK()
} else { } 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) { } catch (err) {
$q.notify({ $q.notify({

@ -1421,5 +1421,6 @@
"tags.searchWithinResultsPlaceholder": "Search within results...", "tags.searchWithinResultsPlaceholder": "Search within results...",
"tags.selectOneMoreTags": "Select one or more tags", "tags.selectOneMoreTags": "Select one or more tags",
"tags.selectOneMoreTagsHint": "Select one or more tags on the left.", "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."
} }

@ -177,7 +177,7 @@ q-layout.admin(view='hHh Lpr lff')
transition-show='jump-up' transition-show='jump-up'
transition-hide='jump-down' transition-hide='jump-down'
) )
component(:is='adminStore.overlay') component(:is='overlays[adminStore.overlay]')
q-footer.admin-footer q-footer.admin-footer
q-bar.justify-center(dense) 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. 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 // COMPONENTS
import AccountMenu from '../components/AccountMenu.vue' import AccountMenu from '../components/AccountMenu.vue'
const GroupEditOverlay = defineAsyncComponent(() => import('../components/GroupEditOverlay.vue')) const overlays = {
const UserEditOverlay = defineAsyncComponent(() => import('../components/UserEditOverlay.vue')) GroupEditOverlay: defineAsyncComponent(() => import('../components/GroupEditOverlay.vue')),
UserEditOverlay: defineAsyncComponent(() => import('../components/UserEditOverlay.vue'))
}
// STORES // STORES

@ -175,7 +175,7 @@ watch(() => adminStore.overlay, (newValue, oldValue) => {
} }
}) })
watch(() => route, () => { watch(() => route.params.id, () => {
checkOverlay() checkOverlay()
}) })
@ -213,7 +213,7 @@ async function load () {
} }
function checkOverlay () { function checkOverlay () {
if (route.params && route.params.id) { if (route.params?.id) {
adminStore.$patch({ adminStore.$patch({
overlayOpts: { id: route.params.id }, overlayOpts: { id: route.params.id },
overlay: 'GroupEditOverlay' overlay: 'GroupEditOverlay'

@ -737,7 +737,7 @@ watch(() => state.targets, (newValue) => {
handleSetupCallback() handleSetupCallback()
} }
}) })
watch(() => route, (to, from) => { watch(() => route.params.id, (to, from) => {
if (!to.params.id) { if (!to.params.id) {
return return
} }

Loading…
Cancel
Save