QuickSearch.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { refDebounced } from '@vueuse/shared'
  4. import { computed } from 'vue'
  5. import {
  6. NotificationTypes,
  7. useNotifications,
  8. } from '#shared/components/CommonNotifications/index.ts'
  9. import { useConfirmation } from '#shared/composables/useConfirmation.ts'
  10. import { useTouchDevice } from '#shared/composables/useTouchDevice.ts'
  11. import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
  12. import QueryHandler from '#shared/server/apollo/handler/QueryHandler.ts'
  13. import SubscriptionHandler from '#shared/server/apollo/handler/SubscriptionHandler.ts'
  14. import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
  15. import CommonSectionCollapse from '#desktop/components/CommonSectionCollapse/CommonSectionCollapse.vue'
  16. import QuickSearchResultList from '#desktop/components/QuickSearch/QuickSearchResultList/QuickSearchResultList.vue'
  17. import { useRecentSearches } from '#desktop/composables/useRecentSearches.ts'
  18. import { useUserCurrentRecentViewResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentViewReset.api.ts'
  19. import { useUserCurrentRecentViewListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentViewList.api.ts'
  20. import { useUserCurrentRecentViewUpdatesSubscription } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentViewUpdates.api.ts'
  21. import { useQuickSearchInput } from './composables/useQuickSearchInput.ts'
  22. import { lookupQuickSearchPluginComponent } from './plugins/index.ts'
  23. const DEBOUNCE_TIME = 400
  24. interface Props {
  25. collapsed?: boolean
  26. search: string
  27. }
  28. const props = defineProps<Props>()
  29. const hasSearchInput = computed(() => props.search?.length > 0)
  30. const debouncedHasSearchInput = refDebounced(hasSearchInput, DEBOUNCE_TIME)
  31. const { isTouchDevice } = useTouchDevice()
  32. const { recentSearches, clearSearches, removeSearch } = useRecentSearches()
  33. const recentViewListQuery = new QueryHandler(
  34. useUserCurrentRecentViewListQuery({
  35. limit: 10,
  36. }),
  37. )
  38. const recentViewListQueryResult = recentViewListQuery.result()
  39. const recentViewListItems = computed(
  40. () => recentViewListQueryResult.value?.userCurrentRecentViewList ?? [],
  41. )
  42. const recentViewUpdatesSubscription = new SubscriptionHandler(
  43. useUserCurrentRecentViewUpdatesSubscription(),
  44. )
  45. recentViewUpdatesSubscription.onResult(({ data }) => {
  46. if (data?.userCurrentRecentViewUpdates) {
  47. recentViewListQuery.refetch()
  48. }
  49. })
  50. const { waitForConfirmation } = useConfirmation()
  51. const { notify } = useNotifications()
  52. const confirmRemoveRecentSearch = async (searchQuery: string) => {
  53. const confirmed = await waitForConfirmation(
  54. __('Are you sure? This recent search will get lost.'),
  55. { fullscreen: true },
  56. )
  57. if (!confirmed) return
  58. removeSearch(searchQuery)
  59. notify({
  60. id: 'recent-search-removed',
  61. type: NotificationTypes.Success,
  62. message: __('Recent search was removed successfully.'),
  63. })
  64. }
  65. const confirmClearRecentSearches = async () => {
  66. const confirmed = await waitForConfirmation(
  67. __('Are you sure? Your recent searches will get lost.'),
  68. { fullscreen: true },
  69. )
  70. if (!confirmed) return
  71. clearSearches()
  72. notify({
  73. id: 'recent-searches-cleared',
  74. type: NotificationTypes.Success,
  75. message: __('Recent searches were cleared successfully.'),
  76. })
  77. }
  78. const recentViewResetMutation = new MutationHandler(
  79. useUserCurrentRecentViewResetMutation(),
  80. )
  81. const confirmClearRecentViewed = async () => {
  82. const confirmed = await waitForConfirmation(
  83. __('Are you sure? Your recently viewed items will get lost.'),
  84. { fullscreen: true },
  85. )
  86. if (!confirmed) return
  87. recentViewResetMutation.send().then(() => {
  88. notify({
  89. id: 'recent-viewed-cleared',
  90. type: NotificationTypes.Success,
  91. message: __('Recently viewed items were cleared successfully.'),
  92. })
  93. })
  94. }
  95. const { resetQuickSearchInputField } = useQuickSearchInput()
  96. </script>
  97. <template>
  98. <div class="overflow-x-hidden overflow-y-auto px-3 py-2.5 outline-none">
  99. <QuickSearchResultList
  100. v-if="debouncedHasSearchInput && hasSearchInput"
  101. :search="search"
  102. :debounce-time="DEBOUNCE_TIME"
  103. />
  104. <template
  105. v-else-if="recentSearches.length > 0 || recentViewListItems.length > 0"
  106. >
  107. <CommonSectionCollapse
  108. v-if="recentSearches.length > 0"
  109. id="page-recent-searches"
  110. :title="__('Recent searches')"
  111. :no-header="collapsed"
  112. no-collapse
  113. >
  114. <template #default="{ headerId }">
  115. <nav :aria-labelledby="headerId">
  116. <ul class="m-0 flex flex-col gap-1 p-0">
  117. <li
  118. v-for="searchQuery in recentSearches"
  119. :key="searchQuery"
  120. class="group/recent-search flex justify-center"
  121. >
  122. <CommonLink
  123. class="relative flex grow items-center gap-2 rounded-md px-2 py-3 text-neutral-400 hover:bg-blue-900 hover:no-underline!"
  124. :link="`/search/${searchQuery}`"
  125. exact-active-class="bg-blue-800! w-full text-white!"
  126. internal
  127. @click="resetQuickSearchInputField"
  128. >
  129. <CommonIcon name="search" size="tiny" />
  130. <CommonLabel class="gap-2 text-white!">
  131. {{ searchQuery }}
  132. </CommonLabel>
  133. <CommonButton
  134. :aria-label="$t('Delete this recent search')"
  135. :class="{
  136. 'opacity-0 transition-opacity': !isTouchDevice,
  137. }"
  138. class="absolute end-2 top-3 justify-end group-hover/recent-search:opacity-100 focus:opacity-100"
  139. icon="x-lg"
  140. size="small"
  141. variant="remove"
  142. @click.stop.prevent="confirmRemoveRecentSearch(searchQuery)"
  143. />
  144. </CommonLink>
  145. </li>
  146. </ul>
  147. <div class="mt-2 mb-1 flex justify-end">
  148. <CommonLink
  149. link="#"
  150. size="small"
  151. @click="confirmClearRecentSearches"
  152. >
  153. {{ $t('Clear recent searches') }}
  154. </CommonLink>
  155. </div>
  156. </nav>
  157. </template>
  158. </CommonSectionCollapse>
  159. <CommonSectionCollapse
  160. v-if="recentViewListItems.length > 0"
  161. id="page-recently-viewed"
  162. :title="__('Recently viewed')"
  163. :no-header="collapsed"
  164. no-collapse
  165. >
  166. <template #default="{ headerId }">
  167. <nav :aria-labelledby="headerId">
  168. <ul class="m-0 flex flex-col gap-1 p-0">
  169. <li
  170. v-for="item in recentViewListItems"
  171. :key="item.id"
  172. class="relative"
  173. >
  174. <component
  175. :is="lookupQuickSearchPluginComponent(item.__typename!)"
  176. :item="item"
  177. mode="recently-viewed"
  178. @click="resetQuickSearchInputField"
  179. />
  180. </li>
  181. </ul>
  182. <div class="mt-2 mb-1 flex justify-end">
  183. <CommonLink
  184. link="#"
  185. size="small"
  186. @click="confirmClearRecentViewed"
  187. >
  188. {{ $t('Clear recently viewed') }}
  189. </CommonLink>
  190. </div>
  191. </nav>
  192. </template>
  193. </CommonSectionCollapse>
  194. </template>
  195. <CommonLabel v-else>
  196. {{
  197. $t('Start typing i.e. the name of a ticket, an organization or a user.')
  198. }}
  199. </CommonLabel>
  200. </div>
  201. </template>