Browse Source

Maintenance: Desktop view - Implement an overflow mechanism for simple table cell content.

Dusan Vuckovic 9 months ago
parent
commit
d384e3a836

+ 19 - 2
app/frontend/apps/desktop/components/CommonSimpleTable/CommonSimpleTable.vue

@@ -1,6 +1,10 @@
 <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
+import { ref } from 'vue'
+
+import CommonLabel from '#shared/components/CommonLabel/CommonLabel.vue'
+
 import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
 import type { MenuItem } from '#desktop/components/CommonPopover/types.ts'
 
@@ -36,6 +40,12 @@ const rowBackgroundClasses = 'bg-blue-200 dark:bg-gray-700'
 
 const columnSeparatorClasses =
   'border-r border-neutral-100 dark:border-gray-900'
+
+const contentCells = ref()
+
+const getTooltipText = (item: TableItem, header: TableHeader) => {
+  return header.truncate ? item[header.key] : undefined
+}
 </script>
 
 <template>
@@ -74,11 +84,14 @@ const columnSeparatorClasses =
         <td
           v-for="header in headers"
           :key="`${item.id}-${header.key}`"
-          class="h-10 p-2.5 text-sm text-gray-100 first:rounded-s-md last:rounded-e-md dark:text-neutral-400"
+          class="h-10 p-2.5 text-sm first:rounded-s-md last:rounded-e-md"
           :class="[
             (index + 1) % 2 && rowBackgroundClasses,
             header.columnSeparator && columnSeparatorClasses,
             cellAlignmentClasses[header.alignContent || 'left'],
+            {
+              'max-w-32 truncate text-black dark:text-white': header.truncate,
+            },
           ]"
         >
           <slot
@@ -86,7 +99,11 @@ const columnSeparatorClasses =
             :item="item"
             :header="header"
           >
-            <CommonLabel class="text-black dark:text-white">
+            <CommonLabel
+              ref="contentCells"
+              v-tooltip.truncateOnly="getTooltipText(item, header)"
+              class="inline text-black dark:text-white"
+            >
               <template v-if="!item[header.key]">-</template>
               <template v-else-if="header.type === 'timestamp'">
                 <CommonDateTime :date-time="item[header.key] as string" />

+ 59 - 0
app/frontend/apps/desktop/components/CommonSimpleTable/__tests__/CommonSimpleTable.spec.ts

@@ -1,5 +1,7 @@
 // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
 
+import { waitFor } from '@testing-library/vue'
+
 import { renderComponent } from '#tests/support/components/index.ts'
 
 import { i18n } from '#shared/i18n.ts'
@@ -112,4 +114,61 @@ describe('CommonSimpleTable.vue', () => {
       `${__filename}.snapshot.txt`,
     )
   })
+
+  it('supports text truncation in cell content', async () => {
+    const view = renderTable({
+      headers: [
+        ...tableHeaders,
+        {
+          key: 'truncated',
+          label: 'Truncated',
+          truncate: true,
+        },
+      ],
+      items: [
+        ...tableItems,
+        {
+          id: 2,
+          name: 'Max Mustermann',
+          role: 'Admin',
+          truncated: 'Some text to be truncated',
+        },
+      ],
+    })
+
+    const truncatedText = view.getByText('Some text to be truncated')
+
+    expect(truncatedText.parentElement).toHaveClass('truncate')
+  })
+
+  it('supports tooltip on truncated cell content', async () => {
+    const view = renderTable({
+      headers: [
+        ...tableHeaders,
+        {
+          key: 'truncated',
+          label: 'Truncated',
+          truncate: true,
+        },
+      ],
+      items: [
+        ...tableItems,
+        {
+          id: 2,
+          name: 'Max Mustermann',
+          role: 'Admin',
+          truncated: 'Some text to be truncated',
+        },
+      ],
+    })
+
+    await view.events.hover(view.getByText('Max Mustermann'))
+
+    await waitFor(() => {
+      expect(view.getByText('Some text to be truncated')).toBeInTheDocument()
+      expect(
+        view.getByLabelText('Some text to be truncated'),
+      ).toBeInTheDocument()
+    })
+  })
 })

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

@@ -38,11 +38,11 @@
     <tr>
       
       <td
-        class="h-10 p-2.5 text-sm text-gray-100 first:rounded-s-md last:rounded-e-md dark:text-neutral-400 bg-blue-200 dark:bg-gray-700 text-left"
+        class="h-10 p-2.5 text-sm first:rounded-s-md last:rounded-e-md bg-blue-200 dark:bg-gray-700 text-left"
       >
         
         <common-label-stub
-          class="text-black dark:text-white"
+          class="inline text-black dark:text-white"
           size="medium"
         />
         
@@ -50,11 +50,11 @@
         
       </td>
       <td
-        class="h-10 p-2.5 text-sm text-gray-100 first:rounded-s-md last:rounded-e-md dark:text-neutral-400 bg-blue-200 dark:bg-gray-700 text-left"
+        class="h-10 p-2.5 text-sm first:rounded-s-md last:rounded-e-md bg-blue-200 dark:bg-gray-700 text-left"
       >
         
         <common-label-stub
-          class="text-black dark:text-white"
+          class="inline text-black dark:text-white"
           size="medium"
         />
         

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

@@ -10,6 +10,7 @@ export interface TableHeader<K = string> {
   columnSeparator?: boolean
   alignContent?: 'center' | 'right'
   type?: TableColumnType
+  truncate?: boolean
   [key: string]: unknown
 }
 export interface TableItem {

+ 1 - 0
app/frontend/apps/desktop/components/TwoFactor/TwoFactorConfiguration/TwoFactorConfigurationSecurityKeys.vue

@@ -98,6 +98,7 @@ const tableHeaders: TableHeader[] = [
   {
     key: 'nickname',
     label: __('Name'),
+    truncate: true,
   },
   {
     key: 'created_at',

+ 49 - 0
app/frontend/apps/desktop/initializer/initializeGlobalDirectives.ts

@@ -0,0 +1,49 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import { type App, type Directive } from 'vue'
+
+import type { ImportGlobEagerDefault } from '#shared/types/utils.ts'
+
+import vTooltip from '#desktop/plugins/directives/tooltip.ts'
+
+interface DirectiveModule {
+  name: string
+  directive: Record<string, unknown> | ((...args: unknown[]) => unknown)
+}
+
+const directiveModules: ImportGlobEagerDefault<DirectiveModule>[] =
+  Object.values(
+    import.meta.glob('../plugins/directives/*', {
+      eager: true,
+    }),
+  )
+
+export const directives: Record<string, Directive> = directiveModules.reduce(
+  (directiveRecord: Record<string, Directive>, module) => {
+    const { name, directive } = module.default
+    directiveRecord[name] = directive
+    return directiveRecord
+  },
+  {},
+)
+
+const initializeGlobalDirectives = (app: App) => {
+  directiveModules.forEach(
+    (module: ImportGlobEagerDefault<DirectiveModule>) => {
+      const { name, directive } = module.default
+      app.directive(name, directive)
+    },
+  )
+}
+
+export default initializeGlobalDirectives
+
+// :TODO improve DX by adding type definitions for global directives
+declare module '@vue/runtime-core' {
+  export interface GlobalDirectives {
+    vTooltip: typeof vTooltip
+  }
+  // export interface ComponentCustomProperties {
+  //   vTooltip: typeof vTooltip.directive
+  // }
+}

+ 2 - 0
app/frontend/apps/desktop/main.ts

@@ -19,6 +19,7 @@ import { twoFactorConfigurationPluginLookup } from '#desktop/entities/two-factor
 import { initializeForm, initializeFormFields } from '#desktop/form/index.ts'
 import { initializeDesktopIcons } from '#desktop/initializer/initializeDesktopIcons.ts'
 import { initializeGlobalComponentStyles } from '#desktop/initializer/initializeGlobalComponentStyles.ts'
+import initializeGlobalDirectives from '#desktop/initializer/initializeGlobalDirectives.ts'
 import { ensureAfterAuth } from '#desktop/pages/authentication/after-auth/composable/useAfterAuthPlugins.ts'
 import initializeRouter from '#desktop/router/index.ts'
 import initializeApolloClient from '#desktop/server/apollo/index.ts'
@@ -39,6 +40,7 @@ export const mountApp = async () => {
   initializeGlobalComponents(app)
   initializeAppName('desktop')
   initializeGlobalProperties(app)
+  initializeGlobalDirectives(app)
   initializeStoreSubscriptions()
   initializeTwoFactorPlugins(twoFactorConfigurationPluginLookup)
 

+ 8 - 4
app/frontend/apps/desktop/pages/dashboard/views/Playground.vue

@@ -856,6 +856,7 @@ const tableHeaders = [
   {
     key: 'title',
     label: 'Job position',
+    truncate: true,
   },
   {
     key: 'email',
@@ -899,14 +900,15 @@ const tableItems = reactive([
   {
     id: 5,
     name: 'Leonard Krasner',
-    title: 'Senior Designer',
+    title: 'Senior Designer Principal Designer ',
     email: 'leonard.krasner@example.com',
     role: 'Owner',
   },
   {
     id: 6,
     name: 'Floyd Miles',
-    title: 'Principal Designer',
+    title:
+      'Principal Designer for a very long way to go to see the end of the title. It is a very long title, indeed.',
     email: 'floyd.miles@example.com',
     role: 'Member',
   },
@@ -944,9 +946,11 @@ const { activeTab: activeFilters } = useTabManager<Tab[]>()
 <template>
   <LayoutContent :breadcrumb-items="[]">
     <div class="w-1/2">
-      <h2 class="text-xl">Buttons</h2>
+      <h1 id="test" v-tooltip="'Hello world'" class="w-fit">Tooltip example</h1>
 
-      <h3>Text only</h3>
+      <h2 title="Buttons" class="text-xl">Buttons</h2>
+
+      <h3 v-tooltip="'another example'">Text only</h3>
       <div class="flex space-x-3 py-2">
         <CommonButton variant="primary" />
         <CommonButton variant="secondary" />

+ 3 - 1
app/frontend/apps/desktop/pages/personal-setting/__tests__/personal-setting-two-factor-auth/personal-setting-two-factor-auth-security-keys.spec.ts

@@ -158,7 +158,9 @@ describe('Two-factor Authentication - Security Keys', () => {
     await waitForUserCurrentTwoFactorGetMethodConfigurationQueryCalls()
 
     expect(flyout).toHaveTextContent('foobar')
-    expect(within(flyout).getByTitle('2024-01-01 00:00')).toBeInTheDocument()
+    expect(
+      within(flyout).getByLabelText('2024-01-01 00:00'),
+    ).toBeInTheDocument()
 
     mockUserCurrentTwoFactorRemoveMethodCredentialsMutation({
       userCurrentTwoFactorRemoveMethodCredentials: {

+ 2 - 0
app/frontend/apps/desktop/pages/personal-setting/views/PersonalSettingDevices.vue

@@ -99,10 +99,12 @@ const tableHeaders: TableHeader[] = [
   {
     key: 'name',
     label: __('Name'),
+    truncate: true,
   },
   {
     key: 'location',
     label: __('Location'),
+    truncate: true,
   },
   {
     key: 'updatedAt',

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