CommonSimpleTable.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. <!-- Copyright (C) 2012-2024 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 type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
  7. import SimpleTableRow from '#desktop/components/CommonSimpleTable/SimpleTableRow.vue'
  8. import { useTableCheckboxes } from '#desktop/components/CommonSimpleTable/useTableCheckboxes.ts'
  9. import type { TableHeader, TableItem } from './types.ts'
  10. export interface Props {
  11. headers: TableHeader[]
  12. items: TableItem[]
  13. actions?: MenuItem[]
  14. onClickRow?: (tableItem: TableItem) => void
  15. /**
  16. * Used to set a default selected row
  17. * Is not used for checkbox
  18. * */
  19. selectedRowId?: string
  20. hasCheckboxColumn?: boolean
  21. }
  22. const props = defineProps<Props>()
  23. defineEmits<{
  24. 'click-row': [TableItem]
  25. }>()
  26. // :INFO - This would only would work on runtime, when keys are computed
  27. // :TODO - Find a way to infer the types on compile time or remove it completely
  28. // defineSlots<{
  29. // [key: `column-cell-${TableHeader['key']}`]: [
  30. // { item: TableHeader; header: TableHeader },
  31. // ]
  32. // [key: `header-suffix-${TableHeader['key']}`]: [{ item: TableHeader }]
  33. // [key: `item-suffix-${TableHeader['key']}`]: [{ item: TableHeader }]
  34. // test: []
  35. // }>()
  36. // Styling
  37. const cellAlignmentClasses = {
  38. right: 'text-right',
  39. center: 'text-center',
  40. left: 'text-left',
  41. }
  42. const tableHeaders = computed(() =>
  43. props.hasCheckboxColumn
  44. ? [
  45. {
  46. key: 'checkbox',
  47. label: __('Select all entries'),
  48. columnClass: 'w-10',
  49. } as TableHeader,
  50. ...props.headers,
  51. ]
  52. : props.headers,
  53. )
  54. const columnSeparatorClasses =
  55. 'border-r border-neutral-100 dark:border-gray-900'
  56. const getTooltipText = (item: TableItem, header: TableHeader) => {
  57. return header.truncate ? item[header.key] : undefined
  58. }
  59. const checkedRows = defineModel<Array<TableItem>>('checkedRows', {
  60. required: false,
  61. default: (props: Props) => props.items.filter((item) => item.checked), // is not reactive by default and making it reactive causes other issues.
  62. })
  63. const {
  64. hasCheckboxId,
  65. allCheckboxRowsSelected,
  66. selectAllRowCheckboxes,
  67. handleCheckboxUpdate,
  68. } = useTableCheckboxes(checkedRows, toRef(props, 'items'))
  69. const rowHandlers = computed(() =>
  70. props.onClickRow || props.hasCheckboxColumn
  71. ? {
  72. 'click-row': (event: TableItem) => {
  73. if (props.onClickRow) props.onClickRow(event)
  74. if (props.hasCheckboxColumn) handleCheckboxUpdate(event)
  75. },
  76. }
  77. : {},
  78. )
  79. </script>
  80. <template>
  81. <table class="pb-3">
  82. <thead>
  83. <th
  84. v-for="header in tableHeaders"
  85. :key="header.key"
  86. class="h-10 p-2.5 text-xs ltr:text-left rtl:text-right"
  87. :class="[
  88. header.columnClass,
  89. header.columnSeparator && columnSeparatorClasses,
  90. ]"
  91. >
  92. <FormKit
  93. v-if="hasCheckboxColumn && header.key === 'checkbox'"
  94. name="checkbox-all-rows"
  95. :aria-label="
  96. allCheckboxRowsSelected
  97. ? $t('Deselect all entries')
  98. : $t('Select all entries')
  99. "
  100. type="checkbox"
  101. :model-value="allCheckboxRowsSelected"
  102. @update:model-value="selectAllRowCheckboxes"
  103. />
  104. <template v-else>
  105. <slot :name="`column-header-${header.key}`" :header="header">
  106. <CommonLabel
  107. class="-:font-normal -:text-stone-200 -:dark:text-neutral-500"
  108. :class="[
  109. cellAlignmentClasses[header.alignContent || 'left'],
  110. header.labelClass || '',
  111. ]"
  112. size="small"
  113. >
  114. {{ $t(header.label, ...(header.labelPlaceholder || [])) }}
  115. </CommonLabel>
  116. </slot>
  117. </template>
  118. <slot :name="`header-suffix-${header.key}`" :item="header" />
  119. </th>
  120. <th v-if="actions" class="h-10 w-0 p-2.5 text-center">
  121. <CommonLabel
  122. class="font-normal text-stone-200 dark:text-neutral-500"
  123. size="small"
  124. >{{ $t('Actions') }}</CommonLabel
  125. >
  126. </th>
  127. </thead>
  128. <tbody>
  129. <SimpleTableRow
  130. v-for="item in items"
  131. :key="item.id"
  132. :item="item"
  133. :is-row-selected="!hasCheckboxColumn && item.id === props.selectedRowId"
  134. :has-checkbox="hasCheckboxColumn"
  135. v-on="rowHandlers"
  136. >
  137. <template #default="{ isRowSelected }">
  138. <td
  139. v-for="header in tableHeaders"
  140. :key="`${item.id}-${header.key}`"
  141. class="h-10 p-2.5 text-sm"
  142. :class="[
  143. header.columnSeparator && columnSeparatorClasses,
  144. cellAlignmentClasses[header.alignContent || 'left'],
  145. {
  146. 'max-w-32 truncate text-black dark:text-white': header.truncate,
  147. },
  148. ]"
  149. >
  150. <FormKit
  151. v-if="hasCheckboxColumn && header.key === 'checkbox'"
  152. :key="`checkbox-${item.id}-${header.key}`"
  153. :name="`checkbox-${item.id}`"
  154. :aria-label="
  155. hasCheckboxId(item.id)
  156. ? $t('Deselect this entry')
  157. : $t('Select this entry')
  158. "
  159. type="checkbox"
  160. alternative-backrgound
  161. :classes="{
  162. decorator:
  163. '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',
  164. decoratorIcon:
  165. 'group-active:formkit-checked:text-white group-hover:formkit-checked:text-black group-hover:formkit-checked:dark:text-white',
  166. }"
  167. :disabled="!!item.disabled"
  168. :model-value="hasCheckboxId(item.id)"
  169. @click="handleCheckboxUpdate(item)"
  170. @keydown.enter="handleCheckboxUpdate(item)"
  171. @keydown.space="handleCheckboxUpdate(item)"
  172. />
  173. <template v-else>
  174. <slot
  175. :name="`column-cell-${header.key}`"
  176. :item="item"
  177. :is-row-selected="isRowSelected"
  178. :header="header"
  179. >
  180. <CommonLink
  181. v-if="header.type === 'link'"
  182. v-tooltip.truncate="getTooltipText(item, header)"
  183. v-bind="item[header.key] as CommonLinkProps"
  184. :class="{
  185. 'ltr:text-black rtl:text-black dark:text-white':
  186. isRowSelected,
  187. }"
  188. 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"
  189. @click.stop
  190. @keydown.stop
  191. >{{ (item[header.key] as MenuItem).label }}
  192. </CommonLink>
  193. <CommonLabel
  194. v-else
  195. v-tooltip.truncate="getTooltipText(item, header)"
  196. 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"
  197. :class="[
  198. {
  199. 'text-black dark:text-white': isRowSelected,
  200. },
  201. ]"
  202. >
  203. <template v-if="!item[header.key]">-</template>
  204. <template v-else-if="header.type === 'timestamp_absolute'">
  205. <CommonDateTime
  206. class="group-focus-visible:text-white"
  207. :class="{
  208. 'text-black dark:text-white': isRowSelected,
  209. }"
  210. :date-time="item[header.key] as string"
  211. type="absolute"
  212. />
  213. </template>
  214. <template v-else-if="header.type === 'timestamp'">
  215. <CommonDateTime
  216. class="group-focus-visible:text-white"
  217. :class="{
  218. 'text-black dark:text-white': isRowSelected,
  219. }"
  220. :date-time="item[header.key] as string"
  221. as
  222. string
  223. />
  224. </template>
  225. <template v-else>
  226. {{ item[header.key] }}
  227. </template>
  228. </CommonLabel>
  229. </slot>
  230. <slot :name="`item-suffix-${header.key}`" :item="item" />
  231. </template>
  232. </td>
  233. <td v-if="actions" class="h-10 p-2.5 text-center">
  234. <slot name="actions" v-bind="{ actions, item }">
  235. <CommonActionMenu
  236. class="flex items-center justify-center"
  237. :actions="actions"
  238. :entity="item"
  239. button-size="medium"
  240. />
  241. </slot>
  242. </td>
  243. </template>
  244. </SimpleTableRow>
  245. </tbody>
  246. </table>
  247. </template>