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