Browse Source

Feature: Desktop-View - Added new components CommonSimpleTable and CommonActionMenu.

Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Benjamin Scharf <bs@zammad.com>
Dominik Klein 10 months ago
parent
commit
fa07a4cab6

+ 100 - 0
app/frontend/apps/desktop/components/CommonActionMenu/CommonActionMenu.vue

@@ -0,0 +1,100 @@
+<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import { computed, ref, toRefs } from 'vue'
+
+import getUuid from '#shared/utils/getUuid.ts'
+import type { ObjectLike } from '#shared/types/utils.ts'
+
+import CommonPopover from '#desktop/components/CommonPopover/CommonPopover.vue'
+import CommonButton from '#desktop/components/CommonButton/CommonButton.vue'
+import CommonPopoverMenu from '#desktop/components/CommonPopover/CommonPopoverMenu.vue'
+import { usePopover } from '#desktop/components/CommonPopover/usePopover.ts'
+import type {
+  ButtonSize,
+  ButtonVariant,
+} from '#desktop/components/CommonButton/types.ts'
+import type {
+  MenuItem,
+  Orientation,
+  Placement,
+} from '#desktop/components/CommonPopover/types.ts'
+import { usePopoverMenu } from '#desktop/components/CommonPopover/usePopoverMenu.ts'
+
+interface Props {
+  actions: MenuItem[]
+  entity?: ObjectLike
+  buttonSize?: ButtonSize
+  buttonVariant?: ButtonVariant
+  placement?: Placement
+  orientation?: Orientation
+  noSingleActionMode?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  buttonSize: 'large',
+  buttonVariant: 'neutral',
+  placement: 'start',
+  orientation: 'autoVertical',
+})
+
+const popoverMenu = ref<InstanceType<typeof CommonPopoverMenu>>()
+
+const { popover, isOpen: popoverIsOpen, popoverTarget, toggle } = usePopover()
+
+const { actions, entity } = toRefs(props)
+const { filteredMenuItems, singleMenuItemPresent, singleMenuItem } =
+  usePopoverMenu(actions, entity, { provides: true })
+
+const entityId = computed(() => props.entity?.id || getUuid())
+const menuId = computed(() => `popover-${entityId.value}`)
+
+const singleActionMode = computed(() => {
+  if (props.noSingleActionMode) return false
+
+  return singleMenuItemPresent.value
+})
+</script>
+
+<template>
+  <div v-if="filteredMenuItems" class="inline-block">
+    <CommonButton
+      v-if="singleActionMode"
+      :size="buttonSize"
+      :variant="buttonVariant"
+      :aria-label="$t(singleMenuItem?.label)"
+      :icon="singleMenuItem?.icon"
+      @click="singleMenuItem?.onClick?.(entity as ObjectLike)"
+    />
+    <CommonButton
+      v-else
+      :id="entity?.id || entityId"
+      ref="popoverTarget"
+      :aria-label="$t('Action menu button')"
+      aria-haspopup="true"
+      :aria-controls="menuId"
+      :class="{
+        'outline outline-1 outline-offset-1 outline-blue-800': popoverIsOpen,
+      }"
+      :size="buttonSize"
+      :variant="buttonVariant"
+      icon="three-dots-vertical"
+      @click="toggle"
+    />
+
+    <CommonPopover
+      v-if="!singleActionMode"
+      :id="menuId"
+      ref="popover"
+      :placement="placement"
+      :orientation="orientation"
+      :owner="popoverTarget"
+    >
+      <CommonPopoverMenu
+        ref="popoverMenu"
+        :entity="entity"
+        :popover="popover"
+      />
+    </CommonPopover>
+  </div>
+</template>

+ 104 - 0
app/frontend/apps/desktop/components/CommonActionMenu/__tests__/CommonActionMenu.spec.ts

@@ -0,0 +1,104 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
+import renderComponent from '#tests/support/components/renderComponent.ts'
+
+const fn = vi.fn()
+describe('CommonActionMenu', () => {
+  let view: ReturnType<typeof renderComponent>
+
+  const actions = [
+    {
+      key: 'delete-foo',
+      label: 'Delete Foo',
+      icon: 'trash3',
+      onClick: ({ id }: { id: string }) => {
+        fn(id)
+      },
+    },
+    {
+      key: 'change-foo',
+      label: 'Change Foo',
+      icon: 'person-gear',
+      onClick: ({ id }: { id: string }) => {
+        fn(id)
+      },
+    },
+  ]
+
+  beforeEach(() => {
+    view = renderComponent(CommonActionMenu, {
+      props: {
+        entity: {
+          id: 'foo-test-action',
+        },
+        actions,
+      },
+    })
+  })
+
+  it('shows action menu button by default', () => {
+    expect(view.getByIconName('three-dots-vertical')).toBeInTheDocument()
+  })
+
+  it('calls onClick handler when action is clicked', async () => {
+    await view.events.click(view.getByIconName('three-dots-vertical'))
+
+    expect(view.getByIconName('trash3')).toBeInTheDocument()
+    expect(view.getByIconName('person-gear')).toBeInTheDocument()
+
+    await view.events.click(view.getByText('Change Foo'))
+
+    expect(fn).toHaveBeenCalledWith('foo-test-action')
+  })
+
+  it('finds corresponding a11y controls', async () => {
+    const id = view
+      .getByLabelText('Action menu button')
+      .getAttribute('aria-controls')
+
+    await view.events.click(view.getByIconName('three-dots-vertical'))
+
+    const popover = document.getElementById(id as string)
+
+    expect(popover?.getAttribute('id')).toEqual(id)
+  })
+
+  describe('single action mode', () => {
+    beforeEach(async () => {
+      await view.rerender({
+        actions: [actions[0]],
+      })
+    })
+
+    it('adds aria label on single action button', () => {
+      expect(view.getByLabelText('Delete Foo')).toBeInTheDocument()
+    })
+
+    it('supports single action mode', () => {
+      expect(
+        view.queryByIconName('three-dots-vertical'),
+      ).not.toBeInTheDocument()
+
+      expect(view.getByIconName('trash3')).toBeInTheDocument()
+    })
+
+    it('calls onClick handler when action is clicked', async () => {
+      await view.events.click(view.getByIconName('trash3'))
+
+      expect(fn).toHaveBeenCalledWith('foo-test-action')
+    })
+
+    it('renders single action if prop is set', async () => {
+      await view.rerender({
+        noSingleActionMode: true,
+      })
+
+      expect(view.queryByIconName('trash3')).not.toBeInTheDocument()
+
+      await view.events.click(view.getByIconName('three-dots-vertical'))
+
+      expect(view.getByIconName('trash3')).toBeInTheDocument()
+    })
+  })
+})

+ 16 - 5
app/frontend/apps/desktop/components/CommonPopover/CommonPopover.vue

@@ -1,7 +1,13 @@
 <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
-import { computed, nextTick, onUnmounted, ref } from 'vue'
+import {
+  type ComponentPublicInstance,
+  computed,
+  nextTick,
+  onUnmounted,
+  ref,
+} from 'vue'
 import { onClickOutside, type UseElementBoundingReturn } from '@vueuse/core'
 import { onKeyUp, useElementBounding, useWindowSize } from '@vueuse/core'
 
@@ -19,7 +25,7 @@ import type {
 } from './types'
 
 export interface Props {
-  owner: HTMLElement | undefined
+  owner: HTMLElement | ComponentPublicInstance | undefined
   orientation?: Orientation
   placement?: Placement
   hideArrow?: boolean
@@ -209,7 +215,9 @@ const updateOwnerAriaExpandedState = () => {
   const element = props.owner
   if (!element) return
 
-  element.ariaExpanded = showPopover.value ? 'true' : 'false'
+  if ('ariaExpanded' in element) {
+    element.ariaExpanded = showPopover.value ? 'true' : 'false'
+  }
 }
 
 const closePopover = (isInteractive = false) => {
@@ -219,7 +227,10 @@ const closePopover = (isInteractive = false) => {
   emit('close')
 
   nextTick(() => {
-    if (!isInteractive) props.owner?.focus()
+    if (!isInteractive && props.owner) {
+      // eslint-disable-next-line no-unused-expressions
+      '$el' in props.owner ? props.owner.$el?.focus?.() : props.owner?.focus?.()
+    }
     updateOwnerAriaExpandedState()
     testFlags.set('common-select.closed')
   })
@@ -293,7 +304,7 @@ const duration = VITE_TEST_MODE ? undefined : { enter: 300, leave: 200 }
         role="region"
         class="popover fixed z-50 flex min-h-9 rounded-xl border border-neutral-100 bg-white antialiased dark:border-gray-900 dark:bg-gray-500"
         :style="popoverStyle"
-        :aria-labelledby="owner?.id"
+        :aria-labelledby="owner && '$el' in owner ? owner.$el?.id : owner?.id"
       >
         <div class="overflow-y-auto">
           <slot />

+ 33 - 37
app/frontend/apps/desktop/components/CommonPopover/CommonPopoverMenu.vue

@@ -1,56 +1,50 @@
 <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
-import { computed, useSlots } from 'vue'
+import { computed, toRefs, useSlots } from 'vue'
 
-import { useSessionStore } from '#shared/stores/session.ts'
-
-import type { CommonPopoverInstance, MenuItem } from './types'
+import type { ObjectLike } from '#shared/types/utils.ts'
+import { usePopoverMenu } from '#desktop/components/CommonPopover/usePopoverMenu.ts'
+import type { CommonPopoverInstance, MenuItem, Variant } from './types'
 import CommonPopoverMenuItem from './CommonPopoverMenuItem.vue'
 
 export interface Props {
   popover: CommonPopoverInstance | undefined
   headerLabel?: string
   items?: MenuItem[]
+  entity?: ObjectLike
 }
 
 const props = defineProps<Props>()
 
-const session = useSessionStore()
-
-const availableItems = computed(() => {
-  if (!props.items || !props.items.length) return null
-
-  return props.items.filter((item) => {
-    if (item.permission) {
-      return session.hasPermission(item.permission)
-    }
+const { items, entity } = toRefs(props)
 
-    if (item.show) {
-      return item.show()
-    }
-
-    return true
-  })
-})
+const { filteredMenuItems } = usePopoverMenu(items, entity)
 
 const slots = useSlots()
 
 const showHeaderLabel = computed(() => {
-  if (!availableItems.value && !slots.default) return false
+  if (!filteredMenuItems.value && !slots.default) return false
 
   return slots.header || props.headerLabel
 })
 
 const onClickItem = (event: MouseEvent, item: MenuItem) => {
   if (item.onClick) {
-    item.onClick(event)
+    item.onClick(props.entity)
   }
 
   if (!item.noCloseOnClick) {
     props.popover?.closePopover()
   }
 }
+
+const getHoverFocusStyles = (variant?: Variant) => {
+  if (variant === 'danger') {
+    return 'focus-within:bg-red-50 hover:bg-red-50 hover:focus-within:bg-red-50 dark:focus-within:bg-red-900 dark:hover:bg-red-900 dark:hover:focus-within:bg-red-900'
+  }
+  return 'focus-within:bg-blue-800 focus-within:text-white hover:bg-blue-600 hover:focus-within:bg-blue-800 dark:hover:bg-blue-900 dark:hover:focus-within:bg-blue-800'
+}
 </script>
 
 <template>
@@ -61,33 +55,35 @@ const onClickItem = (event: MouseEvent, item: MenuItem) => {
       aria-level="2"
       class="p-2 leading-3"
     >
-      <slot name="header"
-        ><CommonLabel
-          size="small"
-          class="text-stone-200 dark:text-neutral-500"
-          >{{ i18n.t(headerLabel) }}</CommonLabel
-        ></slot
-      >
+      <slot name="header">
+        <CommonLabel size="small" class="text-stone-200 dark:text-neutral-500"
+          >{{ i18n.t(headerLabel) }}
+        </CommonLabel>
+      </slot>
     </div>
 
-    <template v-if="availableItems || $slots.default">
+    <template v-if="filteredMenuItems || $slots.default">
       <slot>
         <ul role="menu" v-bind="$attrs" class="flex w-full flex-col">
-          <template v-for="item in availableItems" :key="item.key">
+          <template v-for="item in filteredMenuItems" :key="item.key">
             <li
               role="menuitem"
-              class="group flex items-center justify-between last:rounded-b-[10px] focus-within:bg-blue-800 focus-within:text-white hover:bg-blue-600 hover:focus-within:bg-blue-800 dark:hover:bg-blue-900 dark:hover:focus-within:bg-blue-800"
-              :class="{
-                'first:rounded-t-[10px]': !showHeaderLabel,
-                'border-t border-neutral-100 dark:border-gray-900':
-                  item.separatorTop,
-              }"
+              class="group flex items-center justify-between last:rounded-b-[10px]"
+              :class="[
+                {
+                  'first:rounded-t-[10px]': !showHeaderLabel,
+                  'border-t border-neutral-100 dark:border-gray-900':
+                    item.separatorTop,
+                },
+                getHoverFocusStyles(item.variant),
+              ]"
             >
               <slot :name="`item-${item.key}`" v-bind="item">
                 <component
                   :is="item.component || CommonPopoverMenuItem"
                   class="flex grow p-2.5"
                   :label="item.label"
+                  :variant="item.variant"
                   :link="item.link"
                   :icon="item.icon"
                   :label-placeholder="item.labelPlaceholder"

+ 23 - 3
app/frontend/apps/desktop/components/CommonPopover/CommonPopoverMenuItem.vue

@@ -1,15 +1,34 @@
 <!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
 
 <script setup lang="ts">
+import type { Variant } from '#desktop/components/CommonPopover/types.ts'
+import { computed } from 'vue'
+
 export interface Props {
   label?: string
   labelPlaceholder?: string[]
   link?: string
   linkExternal?: boolean
+  variant?: Variant
   icon?: string
+  labelClass?: string
 }
 
-defineProps<Props>()
+const props = defineProps<Props>()
+
+const variantClass = computed(() => {
+  if (props.variant === 'danger') {
+    return 'text-red-500'
+  }
+  return 'group-focus-within:text-white group-hover:text-black group-hover:group-focus-within:text-white dark:group-hover:text-white'
+})
+
+const iconColor = computed(() => {
+  if (props.variant === 'danger') {
+    return 'text-red-500'
+  }
+  return 'text-stone-200 dark:text-neutral-500 group-hover:text-black dark:group-hover:text-white group-focus-within:text-white group-hover:group-focus-within:text-white'
+})
 </script>
 
 <template>
@@ -21,9 +40,10 @@ defineProps<Props>()
     data-test-id="popover-menu-item"
   >
     <CommonLabel
-      class="gap-2 group-focus-within:text-white group-hover:text-black group-hover:group-focus-within:text-white dark:group-hover:text-white"
+      class="gap-2"
+      :class="[labelClass, variantClass]"
       :prefix-icon="icon"
-      icon-color="text-stone-200 dark:text-neutral-500 group-hover:text-black dark:group-hover:text-white group-focus-within:text-white group-hover:group-focus-within:text-white"
+      :icon-color="iconColor"
     >
       <slot>{{ i18n.t(label, ...(labelPlaceholder || [])) }}</slot>
     </CommonLabel>

+ 27 - 0
app/frontend/apps/desktop/components/CommonPopover/__tests__/CommonPopoverMenu.spec.ts

@@ -9,6 +9,7 @@ import { usePopover } from '../usePopover.ts'
 import type { MenuItem } from '../types.ts'
 
 const html = String.raw
+const fn = vi.fn()
 
 describe('rendering section', () => {
   it('no output without default slot and items', () => {
@@ -187,4 +188,30 @@ describe('rendering section', () => {
 
     expect(view.getByText('Example Menu item')).toBeInTheDocument()
   })
+
+  it('yields entity data on show if prop is passed', async () => {
+    renderComponent(CommonPopoverMenu, {
+      props: {
+        popover: null,
+        headerLabel: 'Test Header',
+        entity: {
+          id: 'example',
+          name: 'vitest',
+        },
+        items: [
+          {
+            label: 'Example',
+            show: (event: { id: string; name: string }) => {
+              fn(event)
+              return true
+            },
+          },
+        ],
+      },
+      router: true,
+      store: true,
+    })
+
+    expect(fn).toBeCalledWith({ id: 'example', name: 'vitest' })
+  })
 })

+ 6 - 2
app/frontend/apps/desktop/components/CommonPopover/types.ts

@@ -4,6 +4,7 @@ import type { Ref, Component } from 'vue'
 
 import type { RequiredPermission } from '#shared/types/permission.ts'
 
+import type { ObjectLike } from '#shared/types/utils.ts'
 import { type Props as ItemProps } from './CommonPopoverMenuItem.vue'
 
 export interface CommonPopoverInstance {
@@ -28,12 +29,15 @@ export type Orientation =
 
 export type Placement = 'start' | 'end'
 
+export type Variant = 'danger'
+
 export interface MenuItem extends ItemProps {
   key: string
   permission?: RequiredPermission
-  show?: () => boolean
+  show?: (entity?: ObjectLike) => boolean
   separatorTop?: boolean
-  onClick?(event: MouseEvent): void
+  onClick?: (entity?: ObjectLike) => void
   noCloseOnClick?: boolean
   component?: Component
+  variant?: Variant
 }

+ 68 - 0
app/frontend/apps/desktop/components/CommonPopover/usePopoverMenu.ts

@@ -0,0 +1,68 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import { inject, computed, provide } from 'vue'
+import type { ComputedRef, Ref } from 'vue'
+import { useSessionStore } from '#shared/stores/session.ts'
+import type { MenuItem } from '#desktop/components/CommonPopover/types.ts'
+import type { ObjectLike } from '#shared/types/utils.ts'
+
+const POPOVER_MENU_SYMBOL = Symbol('popover-menu')
+
+interface UsePopoverMenuReturn {
+  filteredMenuItems: ComputedRef<MenuItem[] | undefined>
+  singleMenuItemPresent: ComputedRef<boolean>
+  singleMenuItem: ComputedRef<MenuItem | undefined>
+}
+
+export const usePopoverMenu = (
+  items: Ref<MenuItem[] | undefined>,
+  entity: Ref<ObjectLike | undefined>,
+  options: { provides?: boolean } = {},
+) => {
+  const injectPopoverMenu = inject<Maybe<UsePopoverMenuReturn>>(
+    POPOVER_MENU_SYMBOL,
+    null,
+  )
+
+  if (injectPopoverMenu) return injectPopoverMenu
+
+  const { provides = false } = options
+
+  const session = useSessionStore()
+
+  const filteredMenuItems = computed(() => {
+    if (!items.value || !items.value.length) return
+
+    return items.value.filter((item) => {
+      if (item.permission) {
+        return session.hasPermission(item.permission)
+      }
+
+      if (item.show) {
+        return item.show(entity?.value)
+      }
+
+      return true
+    })
+  })
+
+  const singleMenuItemPresent = computed(() => {
+    return filteredMenuItems.value?.length === 1
+  })
+
+  const singleMenuItem = computed(() => {
+    if (!singleMenuItemPresent.value) return
+
+    return filteredMenuItems.value?.[0]
+  })
+
+  const providePopoverMenu = {
+    filteredMenuItems,
+    singleMenuItemPresent,
+    singleMenuItem,
+  }
+
+  if (provides) provide(POPOVER_MENU_SYMBOL, providePopoverMenu)
+
+  return providePopoverMenu
+}

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

@@ -0,0 +1,54 @@
+<!-- Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ -->
+
+<script setup lang="ts">
+import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
+import type { MenuItem } from '#desktop/components/CommonPopover/types.ts'
+import type { TableHeader, TableItem } from './types.ts'
+
+export interface Props {
+  headers: TableHeader[]
+  items: TableItem[]
+  actions?: MenuItem[]
+}
+
+defineProps<Props>()
+</script>
+
+<template>
+  <table class="pb-3">
+    <thead>
+      <th
+        v-for="header in headers"
+        :key="header.key"
+        class="h-10 p-2.5 text-xs font-normal text-stone-200 ltr:text-left rtl:text-right dark:text-neutral-500"
+      >
+        <span>{{ $t(header.label, ...(header.labelPlaceholder || [])) }}</span>
+      </th>
+      <th
+        v-if="actions"
+        class="h-10 p-2.5 text-xs font-normal text-stone-200 dark:text-neutral-500"
+      >
+        <span>{{ $t('Actions') }}</span>
+      </th>
+    </thead>
+    <tbody>
+      <tr v-for="(item, index) in items" :key="item.id">
+        <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="{ 'bg-blue-200 dark:bg-gray-700': (index + 1) % 2 }"
+        >
+          <span>{{ item[header.key] || '-' }}</span>
+        </td>
+        <td
+          v-if="actions"
+          class="h-10 p-2.5 text-center first:rounded-s-md last:rounded-e-md"
+          :class="{ 'bg-blue-200 dark:bg-gray-700': (index + 1) % 2 }"
+        >
+          <CommonActionMenu :actions="actions" :entity="item" />
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</template>

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

@@ -0,0 +1,87 @@
+// Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+import { i18n } from '#shared/i18n.ts'
+import { renderComponent } from '#tests/support/components/index.ts'
+import type { MenuItem } from '#desktop/components/CommonPopover/types.ts'
+import CommonSimpleTable, { type Props } from '../CommonSimpleTable.vue'
+
+const tableHeaders = [
+  {
+    key: 'name',
+    label: 'User name',
+  },
+  {
+    key: 'role',
+    label: 'Role',
+  },
+]
+
+const tableItems = [
+  {
+    id: 1,
+    name: 'Lindsay Walton',
+    role: 'Member',
+  },
+]
+
+const tableActions: MenuItem[] = [
+  {
+    key: 'download',
+    label: 'Download this row',
+    icon: 'download',
+  },
+  {
+    key: 'delete',
+    label: 'Delete this row',
+    icon: 'trash3',
+  },
+]
+
+const renderTable = (props: Props) => {
+  return renderComponent(CommonSimpleTable, {
+    props,
+  })
+}
+
+beforeEach(() => {
+  i18n.setTranslationMap(new Map([['Role', 'Rolle']]))
+})
+
+describe('CommonSimpleTable.vue', () => {
+  it('displays the table without actions', async () => {
+    const view = renderTable({
+      headers: tableHeaders,
+      items: tableItems,
+    })
+
+    expect(view.getByText('User name')).toBeInTheDocument()
+    expect(view.getByText('Rolle')).toBeInTheDocument()
+    expect(view.getByText('Lindsay Walton')).toBeInTheDocument()
+    expect(view.getByText('Member')).toBeInTheDocument()
+    expect(view.queryByText('Actions')).toBeNull()
+  })
+
+  it('displays the table with actions', async () => {
+    const view = renderTable({
+      headers: tableHeaders,
+      items: tableItems,
+      actions: tableActions,
+    })
+
+    expect(view.getByText('Actions')).toBeInTheDocument()
+    expect(view.getByLabelText('Action menu button')).toBeInTheDocument()
+  })
+
+  it('generates expected DOM', async () => {
+    // TODO: check if such snappshot test is really the way we want to go.
+    const view = renderTable({
+      headers: tableHeaders,
+      items: tableItems,
+      actions: tableActions,
+    })
+
+    expect(view.baseElement.querySelector('table')).toMatchFileSnapshot(
+      `${__filename}.snapshot.txt`,
+    )
+  })
+})

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