Browse Source

Feature - Desktop view: Implement ticket overviews (iteration 1)

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

+ 4 - 3
app/channels/graphql_channel.rb

@@ -12,10 +12,11 @@ class GraphqlChannel < ApplicationCable::Channel
 
     # context must be kept in sync with GraphqlController!
     context = {
-      sid:          sid,
-      current_user: current_user,
+      sid:             sid,
+      current_user:    current_user,
+      current_user_id: current_user&.id,
       # :channel is required for ActionCableSubscriptions and MUST NOT be used otherwise.
-      channel:      self,
+      channel:         self,
     }
 
     result = UserInfo.with_user_id(current_user&.id) do

+ 4 - 3
app/controllers/graphql_controller.rb

@@ -49,10 +49,11 @@ class GraphqlController < ApplicationController
   def context
     # context must be kept in sync with GraphqlChannel!
     {
-      sid:          session.id,
-      current_user: current_user,
+      sid:             session.id,
+      current_user:    current_user,
+      current_user_id: current_user&.id,
       # :controller is used by login/logout mutations and MUST NOT be used otherwise.
-      controller:   self,
+      controller:      self,
     }
   end
 

+ 2 - 0
app/frontend/apps/desktop/AppDesktop.vue

@@ -20,6 +20,7 @@ import { useSessionStore } from '#shared/stores/session.ts'
 import emitter from '#shared/utils/emitter.ts'
 
 import { initializeConfirmationDialog } from '#desktop/components/CommonConfirmationDialog/initializeConfirmationDialog.ts'
+import { useTicketOverviewsStore } from '#desktop/entities/ticket/stores/ticketOverviews.ts'
 import { useUserCurrentTaskbarTabsStore } from '#desktop/entities/user/current/stores/taskbarTabs.ts'
 
 const router = useRouter()
@@ -71,6 +72,7 @@ watch(
   (newValue, oldValue) => {
     if (!newValue || oldValue) return
 
+    useTicketOverviewsStore()
     useUserCurrentTaskbarTabsStore()
     initializeDefaultObjectAttributes()
   },

+ 9 - 0
app/frontend/apps/desktop/components/CommonBreadcrumb/CommonBreadcrumb.vue

@@ -68,6 +68,15 @@ const sizeClasses = computed(() => {
           }}
         </h1>
 
+        <CommonBadge
+          v-if="item.count !== undefined"
+          class="font-bold leading-snug ltr:ml-1.5 rtl:mr-1.5"
+          size="xs"
+          rounded
+        >
+          {{ item.count }}
+        </CommonBadge>
+
         <CommonIcon
           v-if="idx !== items.length - 1"
           :name="

+ 1 - 0
app/frontend/apps/desktop/components/CommonBreadcrumb/types.ts

@@ -7,4 +7,5 @@ export interface BreadcrumbItem {
   noOptionLabelTranslation?: boolean
   route?: string
   icon?: string
+  count?: number
 }

+ 4 - 3
app/frontend/apps/desktop/components/CommonCalendarPreviewFlyout/CommonCalendarPreviewFlyout.vue

@@ -12,8 +12,8 @@ import openExternalLink from '#shared/utils/openExternalLink.ts'
 
 import CommonFlyout from '#desktop/components/CommonFlyout/CommonFlyout.vue'
 import CommonLoader from '#desktop/components/CommonLoader/CommonLoader.vue'
-import CommonSimpleTable from '#desktop/components/CommonSimpleTable/CommonSimpleTable.vue'
-import type { TableHeader } from '#desktop/components/CommonSimpleTable/types.ts'
+import CommonSimpleTable from '#desktop/components/CommonTable/CommonSimpleTable.vue'
+import type { TableSimpleHeader } from '#desktop/components/CommonTable/types'
 import { useCalendarIcsFileEventsQuery } from '#desktop/entities/calendar/ics-file/graphql/queries/events.api.ts'
 
 interface Props {
@@ -32,7 +32,7 @@ const calendarEventsQuery = new QueryHandler(
 const calendarEventsQueryResult = calendarEventsQuery.result()
 const calendarEventsQueryLoading = calendarEventsQuery.loading()
 
-const tableHeaders: TableHeader[] = [
+const tableHeaders: TableSimpleHeader[] = [
   {
     key: 'summary',
     label: __('Event Summary'),
@@ -95,6 +95,7 @@ const downloadCalendar = () => {
   >
     <CommonLoader :loading="calendarEventsQueryLoading">
       <CommonSimpleTable
+        :caption="__('Preview Calendar')"
         class="mb-4 w-full"
         :headers="tableHeaders"
         :items="tableItems"

+ 5 - 1
app/frontend/apps/desktop/components/CommonObjectAttribute/CommonObjectAttribute.vue

@@ -15,7 +15,11 @@ defineProps<Props>()
       {{ $t(label) }}
     </CommonLabel>
 
-    <CommonLabel size="medium" class="text-gray-100 dark:text-neutral-400">
+    <CommonLabel
+      tag="div"
+      size="medium"
+      class="text-gray-100 dark:text-neutral-400"
+    >
       <slot>{{ body }}</slot>
     </CommonLabel>
   </div>

+ 0 - 266
app/frontend/apps/desktop/components/CommonSimpleTable/CommonSimpleTable.vue

@@ -1,266 +0,0 @@
-<!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
-
-<script setup lang="ts">
-import { computed, toRef } from 'vue'
-
-import type { Props as CommonLinkProps } from '#shared/components/CommonLink/CommonLink.vue'
-
-import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
-import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
-import SimpleTableRow from '#desktop/components/CommonSimpleTable/SimpleTableRow.vue'
-import { useTableCheckboxes } from '#desktop/components/CommonSimpleTable/useTableCheckboxes.ts'
-
-import type { TableHeader, TableItem } from './types.ts'
-
-export interface Props {
-  headers: TableHeader[]
-  items: TableItem[]
-  actions?: MenuItem[]
-  onClickRow?: (tableItem: TableItem) => void
-  /**
-   * Used to set a default selected row
-   * Is not used for checkbox
-   * */
-  selectedRowId?: string
-  hasCheckboxColumn?: boolean
-}
-
-const props = defineProps<Props>()
-
-defineEmits<{
-  'click-row': [TableItem]
-}>()
-
-// :INFO - This would only would work on runtime, when keys are computed
-// :TODO - Find a way to infer the types on compile time or remove it completely
-// defineSlots<{
-//   [key: `column-cell-${TableHeader['key']}`]: [
-//     { item: TableHeader; header: TableHeader },
-//   ]
-//   [key: `header-suffix-${TableHeader['key']}`]: [{ item: TableHeader }]
-//   [key: `item-suffix-${TableHeader['key']}`]: [{ item: TableHeader }]
-//   test: []
-// }>()
-
-//  Styling
-const cellAlignmentClasses = {
-  right: 'text-right',
-  center: 'text-center',
-  left: 'text-left',
-}
-
-const tableHeaders = computed(() =>
-  props.hasCheckboxColumn
-    ? [
-        {
-          key: 'checkbox',
-          label: __('Select all entries'),
-          columnClass: 'w-10',
-        } as TableHeader,
-        ...props.headers,
-      ]
-    : props.headers,
-)
-
-const columnSeparatorClasses =
-  'border-r border-neutral-100 dark:border-gray-900'
-
-const getTooltipText = (item: TableItem, header: TableHeader) => {
-  return header.truncate ? item[header.key] : undefined
-}
-
-const checkedRows = defineModel<Array<TableItem>>('checkedRows', {
-  required: false,
-  default: (props: Props) => props.items.filter((item) => item.checked), // is not reactive by default and making it reactive causes other issues.
-})
-
-const {
-  hasCheckboxId,
-  allCheckboxRowsSelected,
-  selectAllRowCheckboxes,
-  handleCheckboxUpdate,
-} = useTableCheckboxes(checkedRows, toRef(props, 'items'))
-
-const rowHandlers = computed(() =>
-  props.onClickRow || props.hasCheckboxColumn
-    ? {
-        'click-row': (event: TableItem) => {
-          if (props.onClickRow) props.onClickRow(event)
-          if (props.hasCheckboxColumn) handleCheckboxUpdate(event)
-        },
-      }
-    : {},
-)
-</script>
-
-<template>
-  <table class="pb-3">
-    <thead>
-      <th
-        v-for="header in tableHeaders"
-        :key="header.key"
-        class="h-10 p-2.5 text-xs ltr:text-left rtl:text-right"
-        :class="[
-          header.columnClass,
-          header.columnSeparator && columnSeparatorClasses,
-        ]"
-      >
-        <FormKit
-          v-if="hasCheckboxColumn && header.key === 'checkbox'"
-          name="checkbox-all-rows"
-          :aria-label="
-            allCheckboxRowsSelected
-              ? $t('Deselect all entries')
-              : $t('Select all entries')
-          "
-          type="checkbox"
-          :model-value="allCheckboxRowsSelected"
-          @update:model-value="selectAllRowCheckboxes"
-        />
-
-        <template v-else>
-          <slot :name="`column-header-${header.key}`" :header="header">
-            <CommonLabel
-              class="-:font-normal -:text-stone-200 -:dark:text-neutral-500"
-              :class="[
-                cellAlignmentClasses[header.alignContent || 'left'],
-                header.labelClass || '',
-              ]"
-              size="small"
-            >
-              {{ $t(header.label, ...(header.labelPlaceholder || [])) }}
-            </CommonLabel>
-          </slot>
-        </template>
-
-        <slot :name="`header-suffix-${header.key}`" :item="header" />
-      </th>
-      <th v-if="actions" class="h-10 w-0 p-2.5 text-center">
-        <CommonLabel
-          class="font-normal text-stone-200 dark:text-neutral-500"
-          size="small"
-          >{{ $t('Actions') }}</CommonLabel
-        >
-      </th>
-    </thead>
-    <tbody>
-      <SimpleTableRow
-        v-for="item in items"
-        :key="item.id"
-        :item="item"
-        :is-row-selected="!hasCheckboxColumn && item.id === props.selectedRowId"
-        :has-checkbox="hasCheckboxColumn"
-        v-on="rowHandlers"
-      >
-        <template #default="{ isRowSelected }">
-          <td
-            v-for="header in tableHeaders"
-            :key="`${item.id}-${header.key}`"
-            class="h-10 p-2.5 text-sm"
-            :class="[
-              header.columnSeparator && columnSeparatorClasses,
-              cellAlignmentClasses[header.alignContent || 'left'],
-              {
-                'max-w-32 truncate text-black dark:text-white': header.truncate,
-              },
-            ]"
-          >
-            <FormKit
-              v-if="hasCheckboxColumn && header.key === 'checkbox'"
-              :key="`checkbox-${item.id}-${header.key}`"
-              :name="`checkbox-${item.id}`"
-              :aria-label="
-                hasCheckboxId(item.id)
-                  ? $t('Deselect this entry')
-                  : $t('Select this entry')
-              "
-              type="checkbox"
-              alternative-backrgound
-              :classes="{
-                decorator:
-                  'group-active:formkit-checked:border-white group-hover:dark:border-white group-hover:group-active:border-white group-hover:group-active:peer-hover:border-white group-hover:formkit-checked:border-black group-hover:dark:formkit-checked:border-white group-hover:dark:peer-hover:border-white  ltr:group-hover:dark:group-hover:peer-hover:formkit-checked:border-white ltr:group-hover:peer-hover:dark:border-white rtl:group-hover:peer-hover:dark:border-white ltr:group-hover:peer-hover:border-black rtl:group-hover:peer-hover:border-black  group-hover:border-black',
-                decoratorIcon:
-                  'group-active:formkit-checked:text-white group-hover:formkit-checked:text-black group-hover:formkit-checked:dark:text-white',
-              }"
-              :disabled="!!item.disabled"
-              :model-value="hasCheckboxId(item.id)"
-              @click="handleCheckboxUpdate(item)"
-              @keydown.enter="handleCheckboxUpdate(item)"
-              @keydown.space="handleCheckboxUpdate(item)"
-            />
-            <template v-else>
-              <slot
-                :name="`column-cell-${header.key}`"
-                :item="item"
-                :is-row-selected="isRowSelected"
-                :header="header"
-              >
-                <CommonLink
-                  v-if="header.type === 'link'"
-                  v-tooltip.truncate="getTooltipText(item, header)"
-                  v-bind="item[header.key] as CommonLinkProps"
-                  :class="{
-                    'ltr:text-black rtl:text-black dark:text-white':
-                      isRowSelected,
-                  }"
-                  class="truncate text-sm hover:no-underline group-hover:text-black group-focus-visible:text-white group-active:text-white group-hover:dark:text-white"
-                  @click.stop
-                  @keydown.stop
-                  >{{ (item[header.key] as MenuItem).label }}
-                </CommonLink>
-                <CommonLabel
-                  v-else
-                  v-tooltip.truncate="getTooltipText(item, header)"
-                  class="-:text-gray-100 -:dark:text-neutral-400 inline group-hover:text-black group-focus-visible:text-white group-active:text-white group-hover:dark:text-white"
-                  :class="[
-                    {
-                      'text-black dark:text-white': isRowSelected,
-                    },
-                  ]"
-                >
-                  <template v-if="!item[header.key]">-</template>
-                  <template v-else-if="header.type === 'timestamp_absolute'">
-                    <CommonDateTime
-                      class="group-focus-visible:text-white"
-                      :class="{
-                        'text-black dark:text-white': isRowSelected,
-                      }"
-                      :date-time="item[header.key] as string"
-                      type="absolute"
-                    />
-                  </template>
-                  <template v-else-if="header.type === 'timestamp'">
-                    <CommonDateTime
-                      class="group-focus-visible:text-white"
-                      :class="{
-                        'text-black dark:text-white': isRowSelected,
-                      }"
-                      :date-time="item[header.key] as string"
-                      as
-                      string
-                    />
-                  </template>
-                  <template v-else>
-                    {{ item[header.key] }}
-                  </template>
-                </CommonLabel>
-              </slot>
-
-              <slot :name="`item-suffix-${header.key}`" :item="item" />
-            </template>
-          </td>
-          <td v-if="actions" class="h-10 p-2.5 text-center">
-            <slot name="actions" v-bind="{ actions, item }">
-              <CommonActionMenu
-                class="flex items-center justify-center"
-                :actions="actions"
-                :entity="item"
-                button-size="medium"
-              />
-            </slot>
-          </td>
-        </template>
-      </SimpleTableRow>
-    </tbody>
-  </table>
-</template>

+ 0 - 52
app/frontend/apps/desktop/components/CommonSimpleTable/__tests__/CommonSimpleTable.spec.ts.snapshot.txt

@@ -1,52 +0,0 @@
-<table
-  class="pb-3"
->
-  <thead>
-    
-    <th
-      class="h-10 p-2.5 text-xs ltr:text-left rtl:text-right"
-    >
-      
-      <common-label-stub
-        class="-:font-normal -:text-stone-200 -:dark:text-neutral-500 text-left"
-        size="small"
-        tag="span"
-      />
-      
-      
-      
-    </th>
-    <th
-      class="h-10 p-2.5 text-xs ltr:text-left rtl:text-right"
-    >
-      
-      <common-label-stub
-        class="-:font-normal -:text-stone-200 -:dark:text-neutral-500 text-left"
-        size="small"
-        tag="span"
-      />
-      
-      
-      
-    </th>
-    
-    <th
-      class="h-10 w-0 p-2.5 text-center"
-    >
-      <common-label-stub
-        class="font-normal text-stone-200 dark:text-neutral-500"
-        size="small"
-        tag="span"
-      />
-    </th>
-  </thead>
-  <tbody>
-    
-    <simple-table-row-stub
-      hascheckbox="false"
-      isrowselected="false"
-      item="[object Object]"
-    />
-    
-  </tbody>
-</table>

+ 0 - 21
app/frontend/apps/desktop/components/CommonSimpleTable/types.ts

@@ -1,21 +0,0 @@
-// Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
-import type { Props as CommonLinkProps } from '#shared/components/CommonLink/CommonLink.vue'
-
-type TableColumnType = 'timestamp' | 'timestamp_absolute' | 'link'
-
-export interface TableHeader<K = string> {
-  key: K
-  label: string
-  labelPlaceholder?: string[]
-  columnClass?: string
-  columnSeparator?: boolean
-  alignContent?: 'center' | 'right'
-  type?: TableColumnType
-  truncate?: boolean
-  labelClass?: string
-  [key: string]: unknown
-}
-export interface TableItem {
-  [key: string]: unknown | Partial<CommonLinkProps>
-  id: string | number
-}

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