NavigationMenu.vue 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { transform, deburr } from 'lodash-es'
  4. import { computed, ref } from 'vue'
  5. import { i18n } from '#shared/i18n.ts'
  6. import { useSessionStore } from '#shared/stores/session.ts'
  7. import CommonSectionCollapse from '#desktop/components/CommonSectionCollapse/CommonSectionCollapse.vue'
  8. import NavigationMenuFilter from '#desktop/components/NavigationMenu/NavigationMenuFilter.vue'
  9. import NavigationMenuList from '#desktop/components/NavigationMenu/NavigationMenuList.vue'
  10. import type { NavigationMenuCategory, NavigationMenuEntry } from './types'
  11. interface Props {
  12. categories: NavigationMenuCategory[]
  13. entries: Record<string, NavigationMenuEntry[]>
  14. hasNoFiltering?: boolean
  15. }
  16. const props = defineProps<Props>()
  17. const session = useSessionStore()
  18. const permittedEntries = computed(() => {
  19. return transform(
  20. props.entries,
  21. (memo, entries, category) => {
  22. memo[category] = entries.filter((entry) => {
  23. if (
  24. typeof entry.route === 'object' &&
  25. entry.route.meta?.requiredPermission &&
  26. !session.hasPermission(entry.route.meta.requiredPermission)
  27. )
  28. return false
  29. if (typeof entry.show === 'function') return entry.show()
  30. return true
  31. })
  32. },
  33. {} as Record<string, NavigationMenuEntry[]>,
  34. )
  35. })
  36. const searchText = ref('')
  37. const normalizeString = (input: string) => deburr(input).toLocaleLowerCase()
  38. const searchTextMatcher = computed(() => {
  39. if (!searchText.value) return ''
  40. return normalizeString(searchText.value)
  41. })
  42. const isMatchingFilterLabel = (entry: NavigationMenuEntry) => {
  43. return normalizeString(i18n.t(entry.label)).includes(searchTextMatcher.value)
  44. }
  45. const isMatchingFilterKeywords = (entry: NavigationMenuEntry) => {
  46. if (!entry.keywords) return false
  47. return i18n
  48. .t(entry.keywords)
  49. .split(',')
  50. .some((elem) => normalizeString(elem).includes(searchTextMatcher.value))
  51. }
  52. const isMatchingFilter = (entry: NavigationMenuEntry) => {
  53. return isMatchingFilterLabel(entry) || isMatchingFilterKeywords(entry)
  54. }
  55. const allFilteredEntries = computed<NavigationMenuEntry[]>(() => {
  56. return Object.values(permittedEntries.value)
  57. .flat()
  58. .filter((entry) => isMatchingFilter(entry))
  59. })
  60. </script>
  61. <template>
  62. <NavigationMenuFilter v-model.trim="searchText" />
  63. <NavigationMenuList
  64. v-if="searchText && allFilteredEntries.length > 0"
  65. :items="allFilteredEntries"
  66. />
  67. <CommonLabel
  68. v-else-if="allFilteredEntries.length == 0"
  69. class="px-2 py-1 text-stone-200 dark:text-neutral-500"
  70. >{{ __('No results found') }}
  71. </CommonLabel>
  72. <ul v-else>
  73. <li
  74. v-for="category in categories"
  75. :key="category.label"
  76. class="bg-neutral-00 relative z-0 mb-1"
  77. >
  78. <CommonSectionCollapse
  79. v-if="permittedEntries[category.label].length > 0"
  80. :id="category.id"
  81. :title="category.label"
  82. size="large"
  83. no-negative-margin
  84. >
  85. <template #default="{ headerId }">
  86. <NavigationMenuList
  87. :aria-labelledby="headerId"
  88. :items="permittedEntries[category.label]"
  89. />
  90. </template>
  91. </CommonSectionCollapse>
  92. </li>
  93. </ul>
  94. </template>