NavigationMenu.vue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  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 NavigationMenuFilter from '#desktop/components/NavigationMenu/NavigationMenuFilter.vue'
  8. import NavigationMenuHeader from '#desktop/components/NavigationMenu/NavigationMenuHeader.vue'
  9. import NavigationMenuList from '#desktop/components/NavigationMenu/NavigationMenuList.vue'
  10. import { useTransitionCollapse } from '#desktop/composables/useTransitionCollapse.ts'
  11. import type { NavigationMenuCategory, NavigationMenuEntry } from './types'
  12. interface Props {
  13. categories: NavigationMenuCategory[]
  14. entries: Record<string, NavigationMenuEntry[]>
  15. hasNoFiltering?: boolean
  16. }
  17. const props = defineProps<Props>()
  18. const session = useSessionStore()
  19. const permittedEntries = computed(() => {
  20. return transform(
  21. props.entries,
  22. (memo, entries, category) => {
  23. memo[category] = entries.filter((entry) => {
  24. if (
  25. typeof entry.route === 'object' &&
  26. entry.route.meta?.requiredPermission &&
  27. !session.hasPermission(entry.route.meta.requiredPermission)
  28. )
  29. return false
  30. if (typeof entry.show === 'function') return entry.show()
  31. return true
  32. })
  33. },
  34. {} as Record<string, NavigationMenuEntry[]>,
  35. )
  36. })
  37. const searchText = ref('')
  38. const collapsedCategories = ref<Set<string>>(new Set([]))
  39. const toggleCategory = (categoryLabel: string) => {
  40. return collapsedCategories.value.has(categoryLabel)
  41. ? collapsedCategories.value.delete(categoryLabel)
  42. : collapsedCategories.value.add(categoryLabel)
  43. }
  44. const normalizeString = (input: string) => deburr(input).toLocaleLowerCase()
  45. const searchTextMatcher = computed(() => {
  46. if (!searchText.value) return ''
  47. return normalizeString(searchText.value)
  48. })
  49. const isMatchingFilterLabel = (entry: NavigationMenuEntry) => {
  50. return normalizeString(i18n.t(entry.label)).includes(searchTextMatcher.value)
  51. }
  52. const isMatchingFilterKeywords = (entry: NavigationMenuEntry) => {
  53. if (!entry.keywords) return false
  54. return i18n
  55. .t(entry.keywords)
  56. .split(',')
  57. .some((elem) => normalizeString(elem).includes(searchTextMatcher.value))
  58. }
  59. const isMatchingFilter = (entry: NavigationMenuEntry) => {
  60. return isMatchingFilterLabel(entry) || isMatchingFilterKeywords(entry)
  61. }
  62. const allFilteredEntries = computed<NavigationMenuEntry[]>(() => {
  63. return Object.values(permittedEntries.value)
  64. .flat()
  65. .filter((entry) => isMatchingFilter(entry))
  66. })
  67. const { collapseDuration, collapseEnter, collapseAfterEnter, collapseLeave } =
  68. useTransitionCollapse()
  69. </script>
  70. <template>
  71. <NavigationMenuFilter v-model.trim="searchText" />
  72. <NavigationMenuList
  73. v-if="searchText && allFilteredEntries.length > 0"
  74. :items="allFilteredEntries"
  75. />
  76. <CommonLabel
  77. v-else-if="allFilteredEntries.length == 0"
  78. class="px-2 py-1 text-stone-200 dark:text-neutral-500"
  79. >{{ __('No results found') }}
  80. </CommonLabel>
  81. <ul v-else>
  82. <li
  83. v-for="category in categories"
  84. :key="category.label"
  85. class="bg-neutral-00 relative z-0 mb-4"
  86. :class="{ 'overflow-clip': collapsedCategories.has(category.label) }"
  87. >
  88. <template v-if="permittedEntries[category.label].length > 0">
  89. <NavigationMenuHeader
  90. :id="category.id"
  91. class="mb-1 px-2"
  92. :collapsed="collapsedCategories.has(category.label)"
  93. :title="category.label"
  94. :icon="category.icon"
  95. collapsible
  96. @toggle-collapsed="toggleCategory"
  97. />
  98. <Transition
  99. name="collapse"
  100. :duration="collapseDuration"
  101. @enter="collapseEnter"
  102. @after-enter="collapseAfterEnter"
  103. @leave="collapseLeave"
  104. >
  105. <NavigationMenuList
  106. v-show="
  107. permittedEntries[category.label] &&
  108. !collapsedCategories.has(category.label)
  109. "
  110. :id="category.id"
  111. :aria-label="$t(category.label)"
  112. :items="permittedEntries[category.label]"
  113. />
  114. </Transition>
  115. </template>
  116. </li>
  117. </ul>
  118. </template>