From 116cb26f7ecaf8f62521f6de99d1f85fd5a6b9d9 Mon Sep 17 00:00:00 2001 From: Konrad Mohrfeldt <konrad.mohrfeldt@farbdev.org> Date: Mon, 27 May 2024 22:48:40 +0200 Subject: [PATCH] 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 --- .../generic/APermissionGuard.spec.ts | 42 +++++++++++++- src/components/generic/APermissionGuard.vue | 55 +++++++++++++++++-- 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/components/generic/APermissionGuard.spec.ts b/src/components/generic/APermissionGuard.spec.ts index ae5c417b..58eb01cd 100644 --- a/src/components/generic/APermissionGuard.spec.ts +++ b/src/components/generic/APermissionGuard.spec.ts @@ -2,12 +2,18 @@ import { createTestingPinia, TestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' import { defineStore } from 'pinia' import { describe, expect, test, vi } from 'vitest' -import { ref } from 'vue' +import { ref, h, nextTick } from 'vue' import { SteeringUser, useHasUserPermission } from '@/stores/auth' import APermissionGuard from './APermissionGuard.vue' + function createGuard( - content: string, + content: + | string + | ((slotProps: { + disabled?: boolean + areAllChildGuardsHidden: boolean + }) => ReturnType<typeof h>), props: { showPermissions?: string[]; editPermissions?: string[] }, pinia: TestingPinia, ) { @@ -109,4 +115,36 @@ describe('APermissionGuard', () => { const guard = createGuard('my content', { showPermissions: ['my-show-permission'] }, pinia) 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') + }) }) diff --git a/src/components/generic/APermissionGuard.vue b/src/components/generic/APermissionGuard.vue index 9250eef2..89c95552 100644 --- a/src/components/generic/APermissionGuard.vue +++ b/src/components/generic/APermissionGuard.vue @@ -1,8 +1,9 @@ <template> - <slot v-if="hasShowPermissions" v-bind="attrs" /> + <slot v-if="hasShowPerms" :are-all-child-guards-hidden="areAllChildGuardsHidden" v-bind="attrs" /> </template> <script lang="ts"> +import type { InjectionKey, Ref } from 'vue' import type { Authorizer } from '@/stores/auth' type Permissions = Authorizer | string | string[] @@ -10,22 +11,66 @@ export type PermissionGuardProps = { showPermissions?: Permissions editPermissions?: Permissions } + +interface PermissionGuardRegistration { + visible: boolean +} + +const PermissionGuardRegistrations = Symbol('permission-guard-registrations') as InjectionKey< + Ref<Map<string, PermissionGuardRegistration> | null> +> </script> <script lang="ts" setup> -import { computed } from 'vue' +import { computed, inject, onBeforeUnmount, provide, ref, watchEffect } from 'vue' import { useHasUserPermission } from '@/stores/auth' +import { useId } from '@/util' const props = withDefaults(defineProps<PermissionGuardProps>(), { showPermissions: () => [], 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)) -const hasEditPermissions = useHasUserPermission(() => normalizePerms(props.editPermissions)) -const attrs = computed(() => (!hasEditPermissions.value ? { disabled: true } : {})) +// This registration logic collects info on child APermissionGuard +// component instances in order to determine if all of them are hidden. +// 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[] { return typeof permissions === 'string' ? [permissions] : permissions } + +function hasSetPermissions(permissions: Authorizer | string[]) { + return typeof permissions === 'function' || (Array.isArray(permissions) && permissions.length > 0) +} </script> -- GitLab