NavigationMenu.vue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  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. 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 collapsedCategories = ref<Set<string>>(new Set([]))
  38. const toggleCategory = (categoryLabel: string) => {
  39. return collapsedCategories.value.has(categoryLabel)
  40. ? collapsedCategories.value.delete(categoryLabel)
  41. : collapsedCategories.value.add(categoryLabel)
  42. }
  43. const normalizeString = (input: string) => deburr(input).toLocaleLowerCase()
  44. const searchTextMatcher = computed(() => {
  45. if (!searchText.value) return ''
  46. return normalizeString(searchText.value)
  47. })
  48. const isMatchingFilterLabel = (entry: NavigationMenuEntry) => {
  49. return normalizeString(i18n.t(entry.label)).includes(searchTextMatcher.value)
  50. }
  51. const isMatchingFilterKeywords = (entry: NavigationMenuEntry) => {
  52. if (!entry.keywords) return false
  53. return i18n
  54. .t(entry.keywords)
  55. .split(',')
  56. .some((elem) => normalizeString(elem).includes(searchTextMatcher.value))
  57. }
  58. const isMatchingFilter = (entry: NavigationMenuEntry) => {
  59. return isMatchingFilterLabel(entry) || isMatchingFilterKeywords(entry)
  60. }
  61. const allFilteredEntries = computed<NavigationMenuEntry[]>(() => {
  62. return Object.values(permittedEntries.value)
  63. .flat()
  64. .filter((entry) => isMatchingFilter(entry))
  65. })
  66. const { collapseDuration, collapseEnter, collapseAfterEnter, collapseLeave } =
  67. useTransitionCollapse()
  68. </script>
  69. <template>
  70. <NavigationMenuFilter v-model.trim="searchText" />
  71. <NavigationMenuList
  72. v-if="searchText && allFilteredEntries.length > 0"
  73. :items="allFilteredEntries"
  74. />
  75. <CommonLabel
  76. v-else-if="allFilteredEntries.length == 0"
  77. class="px-2 py-1 text-stone-200 dark:text-neutral-500"
  78. >{{ __('No results found') }}
  79. </CommonLabel>
  80. <ul v-else>
  81. <li
  82. v-for="category in categories"
  83. :key="category.label"
  84. class="bg-neutral-00 relative z-0 mb-4"
  85. :class="{ 'overflow-clip': collapsedCategories.has(category.label) }"
  86. >
  87. <template v-if="permittedEntries[category.label].length > 0">
  88. <NavigationMenuHeader
  89. :id="category.id"
  90. class="mb-1 px-2"
  91. :collapsed="collapsedCategories.has(category.label)"
  92. :title="category.label"
  93. :icon="category.icon"
  94. collapsible
  95. @toggle-collapsed="toggleCategory"
  96. />
  97. <Transition
  98. name="collapse"
  99. :duration="collapseDuration"
  100. @enter="collapseEnter"
  101. @after-enter="collapseAfterEnter"
  102. @leave="collapseLeave"
  103. >
  104. <NavigationMenuList
  105. v-show="
  106. permittedEntries[category.label] &&
  107. !collapsedCategories.has(category.label)
  108. "
  109. :id="category.id"
  110. :aria-label="$t(category.label)"
  111. :items="permittedEntries[category.label]"
  112. />
  113. </Transition>
  114. </template>
  115. </li>
  116. </ul>
  117. </template>