Browse Source

Maintenance: Desktop view - Improve handling of tabs in left sidebar.

Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Florian Liebe <fl@zammad.com>
Dusan Vuckovic 7 months ago
parent
commit
254d675550

+ 36 - 18
app/frontend/apps/desktop/components/UserTaskbarTabs/Ticket/Ticket.vue

@@ -9,55 +9,73 @@ import SubscriptionHandler from '#shared/server/apollo/handler/SubscriptionHandl
 
 import CommonTicketStateIndicatorIcon from '#desktop/components/CommonTicketStateIndicatorIcon/CommonTicketStateIndicatorIcon.vue'
 import CommonUpdateIndicator from '#desktop/components/CommonUpdateIndicator/CommonUpdateIndicator.vue'
+import { useUserCurrentTaskbarTabsStore } from '#desktop/entities/user/current/stores/taskbarTabs.ts'
 import { useTicketNumber } from '#desktop/pages/ticket/composables/useTicketNumber.ts'
 
 import type { UserTaskbarTabEntityProps } from '../types.ts'
 
 const props = defineProps<UserTaskbarTabEntityProps<Ticket>>()
 
-const { ticketNumberWithTicketHook } = useTicketNumber(toRef(props, 'entity'))
+const { ticketNumberWithTicketHook } = useTicketNumber(
+  toRef(props.taskbarTab, 'entity'),
+)
 
 const ticketUpdatesSubscription = new SubscriptionHandler(
-  useTicketUpdatesSubscription({ ticketId: props.entity.id, initial: true }), // TODO: maybe only true, when it's not the current active tab, but is it really a overhead?
+  useTicketUpdatesSubscription({
+    ticketId: props.taskbarTab.entity!.id,
+    initial: true,
+  }),
 )
 
 const ticketLink = ref()
 
-const isTicketUpdated = ref(false)
+const isTicketUpdated = computed(() => {
+  if (ticketLink.value?.isExactActive) return false
+  return props.taskbarTab.notify
+})
+
+const { updateTaskbarTab } = useUserCurrentTaskbarTabsStore()
+
+const updateNotifyFlag = (notify: boolean) => {
+  if (!props.taskbarTab.taskbarTabId) return
+
+  updateTaskbarTab(props.taskbarTab.taskbarTabId, {
+    ...props.taskbarTab,
+    notify,
+  })
+}
 
-// TODO: currently after reload the information is gone, in the old interface the "notify" column in the taskbar model was used to remember this.
-// TODO: Idea for the future: Could we not use taskbar.lastContact < ticket.updatedAt instead of a flag?!
-// Not relevant for the initial subscription request.
+// Set the notify flag whenever the result is received from the subscription.
 ticketUpdatesSubscription.onSubscribed().then(() => {
-  // Set the updated flag whenever the result is received from the subscription.
   ticketUpdatesSubscription.onResult(() => {
-    // Skip flagging currently active tab.
-    if (ticketLink.value?.isExactActive) return
+    if (props.taskbarTab.notify) return
 
-    isTicketUpdated.value = true
+    updateNotifyFlag(true)
   })
 })
 
-// Reset the updated flag when the tab becomes active.
+// Reset the notify flag when the tab becomes active.
 watch(
   () => ticketLink.value?.isExactActive,
   (isExactActive) => {
-    if (!isTicketUpdated.value || !isExactActive) return
+    if (!isExactActive && !props.taskbarTab.notify) return
 
-    isTicketUpdated.value = false
+    updateNotifyFlag(false)
   },
 )
 
 const currentState = computed(() => {
-  return props.entity.state.name
+  return props.taskbarTab.entity?.state?.name || ''
 })
 
 const currentTitle = computed(() => {
-  return props.entity.title
+  return props.taskbarTab.entity?.title || ''
 })
 
 const currentStateColorCode = computed(() => {
-  return props.entity.stateColorCode
+  return (
+    props.taskbarTab.entity?.stateColorCode || EnumTicketStateColorCode.Open
+  )
 })
 
 const activeBackgroundColor = computed(() => {
@@ -75,7 +93,7 @@ const activeBackgroundColor = computed(() => {
 })
 
 const currentViewTitle = computed(
-  () => `${ticketNumberWithTicketHook.value} - ${props.entity.title}`,
+  () => `${ticketNumberWithTicketHook.value} - ${currentTitle.value}`,
 )
 </script>
 
@@ -97,7 +115,7 @@ const currentViewTitle = computed(
       />
     </div>
     <CommonLabel
-      class="-:text-gray-300 -:dark:text-neutral-400 line-clamp-1 group-hover/tab:dark:text-white"
+      class="-:text-gray-300 -:dark:text-neutral-400 block truncate group-hover/tab:dark:text-white"
     >
       {{ currentTitle }}
     </CommonLabel>

+ 9 - 11
app/frontend/apps/desktop/components/UserTaskbarTabs/Ticket/TicketCreate.vue

@@ -14,10 +14,6 @@ import type { UserTaskbarTabEntityProps } from '../types.ts'
 const props =
   defineProps<UserTaskbarTabEntityProps<UserTaskbarItemEntityTicketCreate>>()
 
-// TODO: Active tab handling is missing
-
-// TODO: "Subscribe" to form changes and update e.g. the title, the selected type, ...
-
 const { ticketCreateArticleType, defaultTicketCreateArticleType } =
   useTicketCreateArticleType()
 
@@ -26,32 +22,34 @@ const { isTicketCustomer } = useTicketCreateView()
 const currentViewTitle = computed(() => {
   // Customer users should get a generic title prefix, since they cannot control the type of the first article.
   if (isTicketCustomer.value) {
-    if (!props.context?.formValues?.title && !props.entity?.title)
+    if (!props.context?.formValues?.title && !props.taskbarTab.entity?.title)
       return i18n.t('New Ticket')
 
     return i18n.t(
       'New Ticket: %s',
-      (props.context?.formValues?.title as string) || props.entity.title,
+      (props.context?.formValues?.title as string) ||
+        props.taskbarTab.entity?.title,
     )
   }
 
   if (
     !props.context?.formValues?.articleSenderType &&
-    !props.entity?.createArticleTypeKey
+    !props.taskbarTab.entity?.createArticleTypeKey
   )
     return i18n.t(
       ticketCreateArticleType[defaultTicketCreateArticleType]?.label,
     )
 
   const createArticleTypeKey = (props.context?.formValues?.articleSenderType ||
-    props.entity?.createArticleTypeKey) as TicketCreateArticleType
+    props.taskbarTab.entity?.createArticleTypeKey) as TicketCreateArticleType
 
-  if (!props.context?.formValues?.title && !props.entity?.title)
+  if (!props.context?.formValues?.title && !props.taskbarTab.entity?.title)
     return i18n.t(ticketCreateArticleType[createArticleTypeKey]?.label)
 
   return i18n.t(
     ticketCreateArticleType[createArticleTypeKey]?.title,
-    (props.context?.formValues?.title as string) || props.entity.title,
+    (props.context?.formValues?.title as string) ||
+      props.taskbarTab.entity?.title,
   )
 })
 </script>
@@ -72,7 +70,7 @@ const currentViewTitle = computed(() => {
     />
 
     <CommonLabel
-      class="-:text-gray-300 -:dark:text-neutral-400 line-clamp-1 group-hover/tab:text-white"
+      class="-:text-gray-300 -:dark:text-neutral-400 block truncate group-hover/tab:text-white"
     >
       {{ currentViewTitle }}
     </CommonLabel>

+ 1 - 1
app/frontend/apps/desktop/components/UserTaskbarTabs/UserTaskbarTabForbidden.vue

@@ -13,7 +13,7 @@
 
     <CommonLabel
       v-tooltip="$t('You have insufficient rights to open this tab.')"
-      class="line-clamp-1 text-gray-300 dark:text-neutral-400"
+      class="block truncate text-gray-300 dark:text-neutral-400"
     >
       {{ $t('Access denied') }}
     </CommonLabel>

+ 1 - 1
app/frontend/apps/desktop/components/UserTaskbarTabs/UserTaskbarTabNotFound.vue

@@ -13,7 +13,7 @@
 
     <CommonLabel
       v-tooltip="$t('This tab could not be found.')"
-      class="line-clamp-1 text-gray-300 dark:text-neutral-400"
+      class="block truncate text-gray-300 dark:text-neutral-400"
     >
       {{ $t('Not found') }}
     </CommonLabel>

+ 2 - 4
app/frontend/apps/desktop/components/UserTaskbarTabs/UserTaskbarTabs.vue

@@ -181,7 +181,7 @@ const { popover, popoverTarget, toggle, isOpen: popoverIsOpen } = usePopover()
                     ? activeTaskbarTabContext
                     : undefined
                 "
-                :taskbar-tab-id="userTaskbarTab.taskbarTabId"
+                :taskbar-tab="userTaskbarTab"
                 :taskbar-tab-link="
                   getTaskbarTabLink(userTaskbarTab.tabEntityKey)
                 "
@@ -255,9 +255,7 @@ const { popover, popoverTarget, toggle, isOpen: popoverIsOpen } = usePopover()
                   ? activeTaskbarTabContext
                   : undefined
               "
-              :taskbar-tab-id="
-                taskbarTabListByTabEntityKey[tabEntityKey].taskbarTabId
-              "
+              :taskbar-tab="taskbarTabListByTabEntityKey[tabEntityKey]"
               :taskbar-tab-link="getTaskbarTabLink(tabEntityKey)"
               class="active:cursor-grabbing"
             />

+ 15 - 25
app/frontend/apps/desktop/components/UserTaskbarTabs/__tests__/UserTaskbarTabs.spec.ts

@@ -64,6 +64,7 @@ describe('UserTaskbarTabs.vue', () => {
   beforeAll(() => {
     mockApplicationConfig({
       ticket_hook: 'Ticket#',
+      ui_task_mananger_max_task_count: 30,
     })
   })
 
@@ -382,11 +383,17 @@ describe('UserTaskbarTabs.vue', () => {
             createArticleTypeKey: 'phone-in',
           },
           prio: 1,
+          formId: 'foo',
+          changed: false,
+          dirty: false,
+          notify: false,
+          lastContact: '2024-07-25T07:20:32.146Z',
+          updatedAt: '2024-07-24T15:42:28.212Z',
         },
         {
           __typename: 'UserTaskbarItem',
           id: convertToGraphQLId('Taskbar', 2),
-          key: 'TicketCreateScreen-998',
+          key: 'Ticket-42',
           callback: EnumTaskbarEntity.TicketZoom,
           entityAccess: EnumTaskbarEntityAccess.Granted,
           entity: {
@@ -402,6 +409,12 @@ describe('UserTaskbarTabs.vue', () => {
             },
           },
           prio: 2,
+          formId: 'bar',
+          changed: false,
+          dirty: false,
+          notify: false,
+          lastContact: '2024-07-25T07:20:32.146Z',
+          updatedAt: '2024-07-24T15:42:28.212Z',
         },
       ],
     })
@@ -419,39 +432,16 @@ describe('UserTaskbarTabs.vue', () => {
     // Reorder tabs.
     await getUserCurrentTaskbarItemListUpdatesSubscriptionHandler().trigger({
       userCurrentTaskbarItemListUpdates: {
+        __typename: 'UserCurrentTaskbarItemListUpdatesPayload',
         taskbarItemList: [
           {
             __typename: 'UserTaskbarItem',
             id: convertToGraphQLId('Taskbar', 2),
-            key: 'TicketCreateScreen-998',
-            callback: EnumTaskbarEntity.TicketZoom,
-            entityAccess: EnumTaskbarEntityAccess.Granted,
-            entity: {
-              __typename: 'Ticket',
-              id: convertToGraphQLId('Ticket', 42),
-              internalId: 42,
-              number: '53042',
-              title: 'Second ticket',
-              stateColorCode: EnumTicketStateColorCode.Pending,
-              state: {
-                __typename: 'TicketState',
-                name: 'pending reminder',
-              },
-            },
             prio: 1,
           },
           {
             __typename: 'UserTaskbarItem',
             id: convertToGraphQLId('Taskbar', 1),
-            key: 'TicketCreateScreen-999',
-            callback: EnumTaskbarEntity.TicketCreate,
-            entityAccess: EnumTaskbarEntityAccess.Granted,
-            entity: {
-              __typename: 'UserTaskbarItemEntityTicketCreate',
-              uid: '999',
-              title: 'First ticket',
-              createArticleTypeKey: 'phone-in',
-            },
             prio: 2,
           },
         ],

+ 9 - 7
app/frontend/apps/desktop/components/UserTaskbarTabs/types.ts

@@ -12,13 +12,6 @@ import type { TaskbarTabContext } from '#desktop/entities/user/current/types.ts'
 import type { DocumentNode } from 'graphql'
 import type { Component } from 'vue'
 
-export interface UserTaskbarTabEntityProps<T = ObjectWithId> {
-  entity: T
-  taskbarTabId?: ID
-  taskbarTabLink?: string
-  context?: TaskbarTabContext
-}
-
 export interface UserTaskbarTab<T = Maybe<ObjectWithId | ObjectWithUid>> {
   type: EnumTaskbarEntity
   entity?: T
@@ -26,9 +19,18 @@ export interface UserTaskbarTab<T = Maybe<ObjectWithId | ObjectWithUid>> {
   tabEntityKey: string
   taskbarTabId?: ID
   lastContact: Scalars['ISO8601DateTime']['output']
+  updatedAt?: Scalars['ISO8601DateTime']['output']
   order: number
   formId?: Maybe<Scalars['FormId']['input']>
+  changed?: boolean
   dirty?: boolean
+  notify?: boolean
+}
+
+export interface UserTaskbarTabEntityProps<T = ObjectWithId> {
+  taskbarTab: UserTaskbarTab<T>
+  taskbarTabLink?: string
+  context?: TaskbarTabContext
 }
 
 export interface UserTaskbarTabPlugin {

+ 18 - 42
app/frontend/apps/desktop/entities/user/current/composables/useTaskbarTab.ts

@@ -1,8 +1,7 @@
 // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
 
 import { storeToRefs } from 'pinia'
-import { computed, watch, type Ref } from 'vue'
-import { useRoute } from 'vue-router'
+import { computed, onBeforeUnmount, watch, type Ref } from 'vue'
 
 import type { EnumTaskbarEntity } from '#shared/graphql/types.ts'
 
@@ -14,23 +13,24 @@ export const useTaskbarTab = (
   tabEntityType: EnumTaskbarEntity,
   context?: Ref<TaskbarTabContext>,
 ) => {
-  const { activeTaskbarTabContext, activeTaskbarTab } = storeToRefs(
-    useUserCurrentTaskbarTabsStore(),
-  )
-
   const {
-    getTaskbarTabTypePlugin,
-    waitForTaskbarListLoaded,
-    updateTaskbarTab,
-    upsertTaskbarTab,
-    deleteTaskbarTab,
-  } = useUserCurrentTaskbarTabsStore()
+    activeTaskbarTabContext,
+    activeTaskbarTab,
+    activeTaskbarTabEntityKey,
+  } = storeToRefs(useUserCurrentTaskbarTabsStore())
+
+  const { updateTaskbarTab, deleteTaskbarTab } =
+    useUserCurrentTaskbarTabsStore()
 
   // Keep track of the passed context and update the store state accordingly.
   if (context) {
-    watch(context, (newValue) => {
-      activeTaskbarTabContext.value = newValue
-    })
+    watch(
+      context,
+      (newValue) => {
+        activeTaskbarTabContext.value = newValue
+      },
+      { immediate: true },
+    )
   }
 
   watch(
@@ -38,7 +38,6 @@ export const useTaskbarTab = (
     (isDirty) => {
       if (isDirty === undefined || !activeTaskbarTab.value?.taskbarTabId) return
 
-      // Do not update taskbar tab if the dirty flag is in the same state.
       if (activeTaskbarTab.value.dirty === isDirty) return
 
       updateTaskbarTab(activeTaskbarTab.value.taskbarTabId, {
@@ -48,34 +47,11 @@ export const useTaskbarTab = (
     },
   )
 
-  const taskbarTypePlugin = getTaskbarTabTypePlugin(tabEntityType)
-  const route = useRoute()
-
-  const tabEntityInternalId = computed(
-    () => (route.params.internalId || route.params.tabId) as string,
-  )
-
-  const tabEntityKey = computed(() => {
-    return taskbarTypePlugin.buildEntityTabKey(tabEntityInternalId.value)
-  })
-
-  watch(tabEntityInternalId, () => {
-    upsertTaskbarTab(
-      tabEntityType,
-      tabEntityKey.value,
-      tabEntityInternalId.value,
-    )
-  })
-
-  waitForTaskbarListLoaded().then(() => {
-    upsertTaskbarTab(
-      tabEntityType,
-      tabEntityKey.value,
-      tabEntityInternalId.value,
-    )
+  onBeforeUnmount(() => {
+    // Reset active taskbar tab before unmounting the component.
+    activeTaskbarTabEntityKey.value = undefined
   })
 
-  // TODO: use already existing taskbar tab
   const activeTaskbarTabFormId = computed(
     () => activeTaskbarTab.value?.formId || undefined,
   )

+ 3 - 0
app/frontend/apps/desktop/entities/user/current/graphql/fragments/userCurrentTaskbarItemAttributes.api.ts

@@ -29,6 +29,9 @@ export const UserCurrentTaskbarItemAttributesFragmentDoc = gql`
   entityAccess
   prio
   lastContact
+  changed
   dirty
+  notify
+  updatedAt
 }
     ${TicketTaskbarTabAttributesFragmentDoc}`;

+ 3 - 0
app/frontend/apps/desktop/entities/user/current/graphql/fragments/userCurrentTaskbarItemAttributes.graphql

@@ -24,5 +24,8 @@ fragment userCurrentTaskbarItemAttributes on UserTaskbarItem {
   entityAccess
   prio
   lastContact
+  changed
   dirty
+  notify
+  updatedAt
 }

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