CommonSimpleTable.vue 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import { computed, toRef } from 'vue'
  4. import type { Props as CommonLinkProps } from '#shared/components/CommonLink/CommonLink.vue'
  5. import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
  6. import { useTableCheckboxes } from '#desktop/components/CommonTable/composables/useTableCheckboxes.ts'
  7. import TableCaption from '#desktop/components/CommonTable/TableCaption.vue'
  8. import TableRow from '#desktop/components/CommonTable/TableRow.vue'
  9. import { useCellContent } from './composables/useCellContent.ts'
  10. import type {
  11. SimpleTableProps,
  12. TableSimpleHeader,
  13. TableItem,
  14. TableItemLinkValue,
  15. } from './types.ts'
  16. const props = withDefaults(defineProps<SimpleTableProps>(), {
  17. showCaption: false,
  18. })
  19. defineEmits<{
  20. 'click-row': [TableItem]
  21. }>()
  22. // Styling
  23. const cellAlignmentClasses = {
  24. right: 'text-right',
  25. center: 'text-center',
  26. left: 'text-left',
  27. }
  28. const tableHeaders = computed(() => props.headers)
  29. const columnSeparatorClasses =
  30. 'border-r border-neutral-100 dark:border-gray-900'
  31. const getTooltipText = (item: TableItem, header: TableSimpleHeader) => {
  32. return header.truncate ? item[header.key] : undefined
  33. }
  34. const rowHandlers = computed(() =>
  35. props.onClickRow
  36. ? {
  37. 'click-row': (event: TableItem) => {
  38. if (props.onClickRow) props.onClickRow(event)
  39. },
  40. }
  41. : {},
  42. )
  43. const { getCellContentComponent } = useCellContent()
  44. const checkedRows = defineModel<Array<TableItem>>('checkedRows', {
  45. required: false,
  46. default: (props: SimpleTableProps) =>
  47. props.items.filter((item) => item.checked), // is not reactive by default and making it reactive causes other issues.
  48. })
  49. const {
  50. hasCheckboxId,
  51. allCheckboxRowsSelected,
  52. selectAllRowCheckboxes,
  53. handleCheckboxUpdate,
  54. } = useTableCheckboxes(checkedRows, toRef(props, 'items'))
  55. </script>
  56. <template>
  57. <table class="pb-3">
  58. <TableCaption :show="showCaption">{{ caption }}</TableCaption>
  59. <thead>
  60. <tr>
  61. <th
  62. v-for="header in tableHeaders"
  63. :key="header.key"
  64. class="h-10 p-2.5 text-xs ltr:text-left rtl:text-right"
  65. :class="[
  66. header.headerClass,
  67. header.columnSeparator && columnSeparatorClasses,
  68. ]"
  69. >
  70. <FormKit
  71. v-if="hasCheckboxColumn && header.key === 'checkbox'"
  72. name="checkbox-all-rows"
  73. :aria-label="
  74. allCheckboxRowsSelected
  75. ? $t('Deselect all entries')
  76. : $t('Select all entries')
  77. "
  78. type="checkbox"
  79. :model-value="allCheckboxRowsSelected"
  80. @update:model-value="selectAllRowCheckboxes"
  81. />
  82. <slot v-else :name="`column-header-${header.key}`" :header="header">
  83. <CommonLabel
  84. class="-:font-normal -:text-stone-200 -:dark:text-neutral-500"
  85. :class="[
  86. cellAlignmentClasses[header.alignContent || 'left'],
  87. header.labelClass || '',
  88. ]"
  89. size="small"
  90. >
  91. {{ $t(header.label, ...(header.labelPlaceholder || [])) }}
  92. </CommonLabel>
  93. </slot>
  94. <slot :name="`header-suffix-${header.key}`" :item="header" />
  95. </th>
  96. <th v-if="actions" class="h-10 w-0 p-2.5 text-center">
  97. <CommonLabel
  98. class="font-normal text-stone-200 dark:text-neutral-500"
  99. size="small"
  100. >{{ $t('Actions') }}
  101. </CommonLabel>
  102. </th>
  103. </tr>
  104. </thead>
  105. <tbody>
  106. <TableRow
  107. v-for="item in items"
  108. :key="item.id"
  109. :item="item"
  110. :is-row-selected="!hasCheckboxColumn && item.id === props.selectedRowId"
  111. :has-checkbox="hasCheckboxColumn"
  112. v-on="rowHandlers"
  113. >
  114. <template #default="{ isRowSelected }">
  115. <td
  116. v-for="header in tableHeaders"
  117. :key="`${item.id}-${header.key}`"
  118. class="h-10 p-2.5 text-sm"
  119. :class="[
  120. header.columnSeparator && columnSeparatorClasses,
  121. cellAlignmentClasses[header.alignContent || 'left'],
  122. {
  123. 'max-w-32 truncate text-black dark:text-white': header.truncate,
  124. },
  125. ]"
  126. >
  127. <FormKit
  128. v-if="hasCheckboxColumn && header.key === 'checkbox'"
  129. :key="`checkbox-${item.id}-${header.key}`"
  130. :name="`checkbox-${item.id}`"
  131. :aria-label="
  132. hasCheckboxId(item.id)
  133. ? $t('Deselect this entry')
  134. : $t('Select this entry')
  135. "
  136. type="checkbox"
  137. alternative-backrgound
  138. :classes="{
  139. decorator:
  140. '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',
  141. decoratorIcon:
  142. 'group-active:formkit-checked:text-white group-hover:formkit-checked:text-black group-hover:formkit-checked:dark:text-white',
  143. }"
  144. :disabled="!!item.disabled"
  145. :model-value="hasCheckboxId(item.id)"
  146. @click="handleCheckboxUpdate(item)"
  147. @keydown.enter="handleCheckboxUpdate(item)"
  148. @keydown.space="handleCheckboxUpdate(item)"
  149. />
  150. <slot
  151. v-else
  152. :name="`column-cell-${header.key}`"
  153. :item="item"
  154. :is-row-selected="isRowSelected"
  155. :header="header"
  156. >
  157. <CommonLink
  158. v-if="header.type === 'link'"
  159. v-tooltip.truncate="getTooltipText(item, header)"
  160. v-bind="item[header.key] as CommonLinkProps"
  161. :class="{
  162. 'ltr:text-black rtl:text-black dark:text-white':
  163. isRowSelected,
  164. }"
  165. 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"
  166. @click.stop
  167. @keydown.stop
  168. >{{ (item[header.key] as TableItemLinkValue).label }}
  169. </CommonLink>
  170. <CommonLabel
  171. v-else
  172. v-tooltip.truncate="getTooltipText(item, header)"
  173. 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"
  174. :class="[
  175. {
  176. 'text-black dark:text-white': isRowSelected,
  177. },
  178. ]"
  179. >
  180. <template v-if="!item[header.key]">-</template>
  181. <component
  182. :is="getCellContentComponent(header.type)"
  183. v-else
  184. :value="item[header.key] as string"
  185. :is-row-selected="isRowSelected"
  186. />
  187. </CommonLabel>
  188. </slot>
  189. <slot :name="`item-suffix-${header.key}`" :item="item" />
  190. </td>
  191. <td v-if="actions" class="h-10 p-2.5 text-center">
  192. <slot name="actions" v-bind="{ actions, item }">
  193. <CommonActionMenu
  194. class="flex items-center justify-center"
  195. :actions="actions"
  196. :entity="item"
  197. button-size="medium"
  198. />
  199. </slot>
  200. </td>
  201. </template>
  202. </TableRow>
  203. </tbody>
  204. </table>
  205. </template>