Skip to content
Snippets Groups Projects
Commit 116cb26f authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt :koala:
Browse files

feat: allow APermissionGuard to act as container visibility controller

We sometimes want to hide a container if all child APermissionGuard
instances are hidden. This is now possible with the
areAllChildGuardsHidden slot property.

refs #290
parent e8d0b3d4
No related branches found
No related tags found
No related merge requests found
...@@ -2,12 +2,18 @@ import { createTestingPinia, TestingPinia } from '@pinia/testing' ...@@ -2,12 +2,18 @@ import { createTestingPinia, TestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { describe, expect, test, vi } from 'vitest' import { describe, expect, test, vi } from 'vitest'
import { ref } from 'vue' import { ref, h, nextTick } from 'vue'
import { SteeringUser, useHasUserPermission } from '@/stores/auth' import { SteeringUser, useHasUserPermission } from '@/stores/auth'
import APermissionGuard from './APermissionGuard.vue' import APermissionGuard from './APermissionGuard.vue'
function createGuard( function createGuard(
content: string, content:
| string
| ((slotProps: {
disabled?: boolean
areAllChildGuardsHidden: boolean
}) => ReturnType<typeof h>),
props: { showPermissions?: string[]; editPermissions?: string[] }, props: { showPermissions?: string[]; editPermissions?: string[] },
pinia: TestingPinia, pinia: TestingPinia,
) { ) {
...@@ -109,4 +115,36 @@ describe('APermissionGuard', () => { ...@@ -109,4 +115,36 @@ describe('APermissionGuard', () => {
const guard = createGuard('my content', { showPermissions: ['my-show-permission'] }, pinia) const guard = createGuard('my content', { showPermissions: ['my-show-permission'] }, pinia)
expect(guard.text()).toEqual('my content') expect(guard.text()).toEqual('my content')
}) })
test('Passes truthy areAllChildGuardsHidden if every child APermissionGuard component is hidden.', async () => {
const pinia = createPiniaWithFakeUserAuthStore(['my-show-permission'])
const guard = createGuard(
({ areAllChildGuardsHidden }) => {
return h('div', [
h(APermissionGuard, { showPermissions: 'my-other-permission' }, () => ['test: ']),
areAllChildGuardsHidden ? 'has-no-visible-guards' : 'has-visible-guards',
])
},
{},
pinia,
)
await nextTick()
expect(guard.text()).toEqual('has-no-visible-guards')
})
test('Passes falsy areAllChildGuardsHidden if any child APermissionGuard component is visible.', async () => {
const pinia = createPiniaWithFakeUserAuthStore(['my-show-permission'])
const guard = createGuard(
({ areAllChildGuardsHidden }) => {
return h('div', [
h(APermissionGuard, { showPermissions: 'my-show-permission' }, () => ['test: ']),
areAllChildGuardsHidden ? 'has-no-visible-guards' : 'has-visible-guards',
])
},
{},
pinia,
)
await nextTick()
expect(guard.text()).toEqual('test: has-visible-guards')
})
}) })
<template> <template>
<slot v-if="hasShowPermissions" v-bind="attrs" /> <slot v-if="hasShowPerms" :are-all-child-guards-hidden="areAllChildGuardsHidden" v-bind="attrs" />
</template> </template>
<script lang="ts"> <script lang="ts">
import type { InjectionKey, Ref } from 'vue'
import type { Authorizer } from '@/stores/auth' import type { Authorizer } from '@/stores/auth'
type Permissions = Authorizer | string | string[] type Permissions = Authorizer | string | string[]
...@@ -10,22 +11,66 @@ export type PermissionGuardProps = { ...@@ -10,22 +11,66 @@ export type PermissionGuardProps = {
showPermissions?: Permissions showPermissions?: Permissions
editPermissions?: Permissions editPermissions?: Permissions
} }
interface PermissionGuardRegistration {
visible: boolean
}
const PermissionGuardRegistrations = Symbol('permission-guard-registrations') as InjectionKey<
Ref<Map<string, PermissionGuardRegistration> | null>
>
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed, inject, onBeforeUnmount, provide, ref, watchEffect } from 'vue'
import { useHasUserPermission } from '@/stores/auth' import { useHasUserPermission } from '@/stores/auth'
import { useId } from '@/util'
const props = withDefaults(defineProps<PermissionGuardProps>(), { const props = withDefaults(defineProps<PermissionGuardProps>(), {
showPermissions: () => [], showPermissions: () => [],
editPermissions: () => [], editPermissions: () => [],
}) })
defineSlots<{
default(props: { disabled?: boolean; areAllChildGuardsHidden: boolean }): unknown
}>()
const id = useId('permission-guard')
const showPerms = computed(() => normalizePerms(props.showPermissions))
const editPerms = computed(() => normalizePerms(props.editPermissions))
const hasShowPerms = useHasUserPermission(showPerms)
const hasEditPerms = useHasUserPermission(editPerms)
const attrs = computed(() => (!hasEditPerms.value ? { disabled: true } : {}))
const hasShowPermissions = useHasUserPermission(() => normalizePerms(props.showPermissions)) // This registration logic collects info on child APermissionGuard
const hasEditPermissions = useHasUserPermission(() => normalizePerms(props.editPermissions)) // component instances in order to determine if all of them are hidden.
const attrs = computed(() => (!hasEditPermissions.value ? { disabled: true } : {})) // This makes it possible to hide a container element if all of its contents are hidden
// without having to redefine all the show permissions used by the child guards.
const parentRegistration = inject(PermissionGuardRegistrations, ref(null))
const localRegistrations = ref(new Map<string, PermissionGuardRegistration>())
const areAllChildGuardsHidden = computed(() => {
const _registrations = Array.from(localRegistrations.value.values())
if (_registrations.length === 0) return false
return _registrations.every((r) => r.visible === false)
})
provide(PermissionGuardRegistrations, localRegistrations)
watchEffect(() => {
const _registrations = parentRegistration.value
const visible = hasSetPermissions(showPerms.value)
? hasShowPerms.value
: areAllChildGuardsHidden.value
if (_registrations) _registrations.set(id.value, { visible })
})
onBeforeUnmount(() => {
const _registrations = parentRegistration.value
if (_registrations) _registrations.delete(id.value)
})
function normalizePerms(permissions: Authorizer | string | string[]): Authorizer | string[] { function normalizePerms(permissions: Authorizer | string | string[]): Authorizer | string[] {
return typeof permissions === 'string' ? [permissions] : permissions return typeof permissions === 'string' ? [permissions] : permissions
} }
function hasSetPermissions(permissions: Authorizer | string[]) {
return typeof permissions === 'function' || (Array.isArray(permissions) && permissions.length > 0)
}
</script> </script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment