Browse Source

Feature: Desktop view - Add additional improvements to basic detail search.

Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Benjamin Scharf <bs@zammad.com>
Co-authored-by: Florian Liebe <fl@zammad.com>
Benjamin Scharf 4 days ago
parent
commit
17461c7a4a

+ 2 - 0
app/frontend/apps/desktop/components/CommonTable/CommonAdvancedTable.vue

@@ -30,6 +30,7 @@ import {
 } from '#shared/graphql/types.ts'
 } from '#shared/graphql/types.ts'
 import { i18n } from '#shared/i18n.ts'
 import { i18n } from '#shared/i18n.ts'
 import type { ObjectLike } from '#shared/types/utils.ts'
 import type { ObjectLike } from '#shared/types/utils.ts'
+import emitter from '#shared/utils/emitter.ts'
 
 
 import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
 import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
 import CommonTableRowsSkeleton from '#desktop/components/CommonTable/Skeleton/CommonTableRowsSkeleton.vue'
 import CommonTableRowsSkeleton from '#desktop/components/CommonTable/Skeleton/CommonTableRowsSkeleton.vue'
@@ -258,6 +259,7 @@ onMounted(() => {
 })
 })
 
 
 useEventListener('resize', () => initializeHeaderWidths())
 useEventListener('resize', () => initializeHeaderWidths())
+emitter.on('main-sidebar-transition', () => initializeHeaderWidths())
 
 
 const getTooltipText = (
 const getTooltipText = (
   item: TableAdvancedItem,
   item: TableAdvancedItem,

+ 23 - 0
app/frontend/apps/desktop/components/CommonTable/composables/useSkeletonLoadingCount.ts

@@ -0,0 +1,23 @@
+// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+import { useWindowSize } from '@vueuse/core'
+import { computed, type ComputedRef, type Ref } from 'vue'
+
+export const useSkeletonLoadingCount = (
+  count: Ref<number | undefined> | ComputedRef<number | undefined>,
+) => {
+  const { height: screenHeight } = useWindowSize()
+
+  const visibleSkeletonLoadingCount = computed(() => {
+    const maxVisibleRowCount = Math.ceil(screenHeight.value / 40)
+
+    if (count.value && count.value > maxVisibleRowCount)
+      return maxVisibleRowCount
+
+    return count.value
+  })
+
+  return {
+    visibleSkeletonLoadingCount,
+  }
+}

+ 17 - 7
app/frontend/apps/desktop/components/Search/QuickSearch/QuickSearch.vue

@@ -1,7 +1,7 @@
 <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
 <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { refDebounced } from '@vueuse/shared'
+import { refDebounced, watchDebounced } from '@vueuse/shared'
 import { computed } from 'vue'
 import { computed } from 'vue'
 
 
 import {
 import {
@@ -9,6 +9,7 @@ import {
   useNotifications,
   useNotifications,
 } from '#shared/components/CommonNotifications/index.ts'
 } from '#shared/components/CommonNotifications/index.ts'
 import { useConfirmation } from '#shared/composables/useConfirmation.ts'
 import { useConfirmation } from '#shared/composables/useConfirmation.ts'
+import { useRecentSearches } from '#shared/composables/useRecentSearches.ts'
 import { useTouchDevice } from '#shared/composables/useTouchDevice.ts'
 import { useTouchDevice } from '#shared/composables/useTouchDevice.ts'
 import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
 import MutationHandler from '#shared/server/apollo/handler/MutationHandler.ts'
 import QueryHandler from '#shared/server/apollo/handler/QueryHandler.ts'
 import QueryHandler from '#shared/server/apollo/handler/QueryHandler.ts'
@@ -17,7 +18,6 @@ import SubscriptionHandler from '#shared/server/apollo/handler/SubscriptionHandl
 import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
 import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
 import CommonSectionCollapse from '#desktop/components/CommonSectionCollapse/CommonSectionCollapse.vue'
 import CommonSectionCollapse from '#desktop/components/CommonSectionCollapse/CommonSectionCollapse.vue'
 import QuickSearchResultList from '#desktop/components/Search/QuickSearch/QuickSearchResultList/QuickSearchResultList.vue'
 import QuickSearchResultList from '#desktop/components/Search/QuickSearch/QuickSearchResultList/QuickSearchResultList.vue'
-import { useRecentSearches } from '#desktop/composables/useRecentSearches.ts'
 import { useUserCurrentRecentViewResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentViewReset.api.ts'
 import { useUserCurrentRecentViewResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentViewReset.api.ts'
 import { useUserCurrentRecentViewListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentViewList.api.ts'
 import { useUserCurrentRecentViewListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentViewList.api.ts'
 import { useUserCurrentRecentViewUpdatesSubscription } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentViewUpdates.api.ts'
 import { useUserCurrentRecentViewUpdatesSubscription } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentViewUpdates.api.ts'
@@ -36,6 +36,7 @@ interface Props {
 const props = defineProps<Props>()
 const props = defineProps<Props>()
 
 
 const hasSearchInput = computed(() => props.search?.length > 0)
 const hasSearchInput = computed(() => props.search?.length > 0)
+
 const debouncedHasSearchInput = refDebounced(
 const debouncedHasSearchInput = refDebounced(
   hasSearchInput,
   hasSearchInput,
   DEBOUNCE_TIME + 100, // Add some more delay to the first entered debounced value.
   DEBOUNCE_TIME + 100, // Add some more delay to the first entered debounced value.
@@ -43,7 +44,13 @@ const debouncedHasSearchInput = refDebounced(
 
 
 const { isTouchDevice } = useTouchDevice()
 const { isTouchDevice } = useTouchDevice()
 
 
-const { recentSearches, clearSearches, removeSearch } = useRecentSearches()
+const {
+  ADD_RECENT_SEARCH_DEBOUNCE_TIME,
+  recentSearches,
+  addSearch,
+  clearSearches,
+  removeSearch,
+} = useRecentSearches()
 
 
 const recentViewListQuery = new QueryHandler(
 const recentViewListQuery = new QueryHandler(
   useUserCurrentRecentViewListQuery({
   useUserCurrentRecentViewListQuery({
@@ -67,12 +74,16 @@ recentViewUpdatesSubscription.onResult(({ data }) => {
   }
   }
 })
 })
 
 
+watchDebounced(() => props.search, addSearch, {
+  debounce: ADD_RECENT_SEARCH_DEBOUNCE_TIME,
+})
+
 const { waitForConfirmation } = useConfirmation()
 const { waitForConfirmation } = useConfirmation()
 const { notify } = useNotifications()
 const { notify } = useNotifications()
 
 
 const confirmRemoveRecentSearch = async (searchQuery: string) => {
 const confirmRemoveRecentSearch = async (searchQuery: string) => {
   const confirmed = await waitForConfirmation(
   const confirmed = await waitForConfirmation(
-    __('Are you sure? This recent search will get lost.'),
+    __('Are you sure? This recent search will be lost.'),
     { fullscreen: true },
     { fullscreen: true },
   )
   )
 
 
@@ -89,7 +100,7 @@ const confirmRemoveRecentSearch = async (searchQuery: string) => {
 
 
 const confirmClearRecentSearches = async () => {
 const confirmClearRecentSearches = async () => {
   const confirmed = await waitForConfirmation(
   const confirmed = await waitForConfirmation(
-    __('Are you sure? Your recent searches will get lost.'),
+    __('Are you sure? Your recent searches will be lost.'),
     { fullscreen: true },
     { fullscreen: true },
   )
   )
 
 
@@ -150,14 +161,13 @@ const { resetQuickSearchInputField } = useQuickSearchInput()
           <nav :aria-labelledby="headerId">
           <nav :aria-labelledby="headerId">
             <ul class="m-0 flex flex-col gap-1 p-0">
             <ul class="m-0 flex flex-col gap-1 p-0">
               <li
               <li
-                v-for="searchQuery in recentSearches"
+                v-for="searchQuery in [...recentSearches].reverse()"
                 :key="searchQuery"
                 :key="searchQuery"
                 class="group/recent-search flex justify-center"
                 class="group/recent-search flex justify-center"
               >
               >
                 <CommonLink
                 <CommonLink
                   class="relative flex grow items-center gap-2 rounded-md px-2 py-3 text-neutral-400 hover:bg-blue-900 hover:no-underline!"
                   class="relative flex grow items-center gap-2 rounded-md px-2 py-3 text-neutral-400 hover:bg-blue-900 hover:no-underline!"
                   :link="`/search/${searchQuery}`"
                   :link="`/search/${searchQuery}`"
-                  exact-active-class="bg-blue-800! w-full text-white!"
                   internal
                   internal
                   @click="resetQuickSearchInputField"
                   @click="resetQuickSearchInputField"
                 >
                 >

+ 1 - 1
app/frontend/apps/desktop/components/Search/QuickSearch/__tests__/QuickSearch.spec.ts

@@ -6,13 +6,13 @@ import renderComponent from '#tests/support/components/renderComponent.ts'
 import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
 import { mockUserCurrent } from '#tests/support/mock-userCurrent.ts'
 import { waitForNextTick } from '#tests/support/utils.ts'
 import { waitForNextTick } from '#tests/support/utils.ts'
 
 
+import { useRecentSearches } from '#shared/composables/useRecentSearches.ts'
 import {
 import {
   EnumTicketStateColorCode,
   EnumTicketStateColorCode,
   type Organization,
   type Organization,
 } from '#shared/graphql/types.ts'
 } from '#shared/graphql/types.ts'
 import { convertToGraphQLId } from '#shared/graphql/utils.ts'
 import { convertToGraphQLId } from '#shared/graphql/utils.ts'
 
 
-import { useRecentSearches } from '#desktop/composables/useRecentSearches.ts'
 import { mockUserCurrentRecentViewResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentViewReset.mocks.ts'
 import { mockUserCurrentRecentViewResetMutation } from '#desktop/entities/user/current/graphql/mutations/userCurrentRecentViewReset.mocks.ts'
 import { mockUserCurrentRecentViewListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentViewList.mocks.ts'
 import { mockUserCurrentRecentViewListQuery } from '#desktop/entities/user/current/graphql/queries/userCurrentRecentViewList.mocks.ts'
 import { getUserCurrentRecentViewUpdatesSubscriptionHandler } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentViewUpdates.mocks.ts'
 import { getUserCurrentRecentViewUpdatesSubscriptionHandler } from '#desktop/entities/user/current/graphql/subscriptions/userCurrentRecentViewUpdates.mocks.ts'

+ 4 - 0
app/frontend/apps/desktop/components/Search/graphql/queries/detailSearch.api.ts

@@ -26,6 +26,10 @@ export const DetailSearchDocument = gql`
           id
           id
           fullname
           fullname
         }
         }
+        owner {
+          id
+          fullname
+        }
         group {
         group {
           id
           id
           name
           name

+ 4 - 0
app/frontend/apps/desktop/components/Search/graphql/queries/detailSearch.graphql

@@ -25,6 +25,10 @@ query detailSearch(
           id
           id
           fullname
           fullname
         }
         }
+        owner {
+          id
+          fullname
+        }
         group {
         group {
           id
           id
           name
           name

+ 7 - 0
app/frontend/apps/desktop/components/User/UserListTable.vue

@@ -61,6 +61,13 @@ const { goToItem, goToItemLinkColumn, loadMore, resort, storageKeyId } =
           dataType: 'input',
           dataType: 'input',
         },
         },
       ]"
       ]"
+      :attribute-extensions="{
+        organization_ids: {
+          headerPreferences: {
+            noSorting: true,
+          },
+        },
+      }"
       :items="items"
       :items="items"
       :total-items="totalCount"
       :total-items="totalCount"
       :storage-key-id="storageKeyId"
       :storage-key-id="storageKeyId"

+ 10 - 5
app/frontend/apps/desktop/components/UserTaskbarTabs/Search/Search.vue

@@ -3,20 +3,25 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import { computed, toRef } from 'vue'
 import { computed, toRef } from 'vue'
 
 
+import type { UserTaskbarItemEntitySearch } from '#shared/graphql/types.ts'
+
 import { useUserTaskbarTabLink } from '#desktop/composables/useUserTaskbarTabLink.ts'
 import { useUserTaskbarTabLink } from '#desktop/composables/useUserTaskbarTabLink.ts'
 
 
 import type { UserTaskbarTabEntityProps } from '../types.ts'
 import type { UserTaskbarTabEntityProps } from '../types.ts'
 
 
-const props = defineProps<UserTaskbarTabEntityProps>()
+const props =
+  defineProps<UserTaskbarTabEntityProps<UserTaskbarItemEntitySearch>>()
 
 
 const { tabLinkInstance, taskbarTabActive } = useUserTaskbarTabLink(
 const { tabLinkInstance, taskbarTabActive } = useUserTaskbarTabLink(
   toRef(props, 'taskbarTab'),
   toRef(props, 'taskbarTab'),
 )
 )
 
 
-const currentTitle = computed(() => {
-  console.log('taskbar props', props)
-  return __('Extended Search')
-})
+const currentTitle = computed(
+  () =>
+    props.context?.query ||
+    props.taskbarTab.entity?.query ||
+    __('Extended Search'),
+)
 </script>
 </script>
 
 
 <template>
 <template>

+ 18 - 12
app/frontend/apps/desktop/components/UserTaskbarTabs/plugins/search.ts

@@ -1,7 +1,10 @@
 // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
 // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
 
 
 import { TicketTaskbarTabAttributesFragmentDoc } from '#shared/entities/ticket/graphql/fragments/ticketTaskbarTabAttributes.api.ts'
 import { TicketTaskbarTabAttributesFragmentDoc } from '#shared/entities/ticket/graphql/fragments/ticketTaskbarTabAttributes.api.ts'
-import { EnumTaskbarEntity } from '#shared/graphql/types.ts'
+import {
+  EnumTaskbarEntity,
+  type UserTaskbarItemEntitySearch,
+} from '#shared/graphql/types.ts'
 
 
 import type { UserTaskbarTabPlugin } from '#desktop/components/UserTaskbarTabs/types.ts'
 import type { UserTaskbarTabPlugin } from '#desktop/components/UserTaskbarTabs/types.ts'
 
 
@@ -15,16 +18,19 @@ export default <UserTaskbarTabPlugin>{
   entityType,
   entityType,
   entityDocument: TicketTaskbarTabAttributesFragmentDoc,
   entityDocument: TicketTaskbarTabAttributesFragmentDoc,
   buildEntityTabKey: () => entityType,
   buildEntityTabKey: () => entityType,
-  buildTaskbarTabParams: (entityInternalId: string) => {
-    return {
-      search: entityInternalId,
-    }
-  },
-  buildTaskbarTabLink: (entity) => {
-    console.log('search entity', entity)
-    // :TODO add search term and query for entity
-    return '/search'
-    // return `/search/${entity}`
+  buildTaskbarTabEntityId: () => undefined,
+  buildTaskbarTabParams: (route) => ({
+    query: route.params.searchTerm,
+    model: route.query.entity,
+  }),
+  buildTaskbarTabLink: (entity: UserTaskbarItemEntitySearch) => {
+    const { query, model } = entity
+
+    let url = '/search'
+    if (query) url += `/${query}`
+    if (model) url += `?entity=${model}`
+
+    return encodeURI(url)
   },
   },
-  confirmTabRemove: true,
+  confirmTabRemove: false,
 }
 }

+ 4 - 7
app/frontend/apps/desktop/components/UserTaskbarTabs/plugins/ticket.ts

@@ -17,13 +17,9 @@ export default <UserTaskbarTabPlugin>{
   component: Ticket,
   component: Ticket,
   entityType,
   entityType,
   entityDocument: TicketTaskbarTabAttributesFragmentDoc,
   entityDocument: TicketTaskbarTabAttributesFragmentDoc,
-  buildEntityTabKey: (entityInternalId: string) =>
-    `${entityType}-${entityInternalId}`,
-  buildTaskbarTabParams: (entityInternalId: string) => {
-    return {
-      ticket_id: entityInternalId,
-    }
-  },
+  buildEntityTabKey: (route) => `${entityType}-${route.params.internalId}`,
+  buildTaskbarTabEntityId: (route) => route.params.internalId,
+  buildTaskbarTabParams: (route) => ({ ticket_id: route.params.internalId }),
   buildTaskbarTabLink: (entity?: TicketType, entityKey?: string) => {
   buildTaskbarTabLink: (entity?: TicketType, entityKey?: string) => {
     if (!entity?.internalId) {
     if (!entity?.internalId) {
       if (!entityKey) return
       if (!entityKey) return
@@ -32,4 +28,5 @@ export default <UserTaskbarTabPlugin>{
     return `/tickets/${entity.internalId}`
     return `/tickets/${entity.internalId}`
   },
   },
   confirmTabRemove: true,
   confirmTabRemove: true,
+  touchExistingTab: true,
 }
 }

Some files were not shown because too many files changed in this diff