CommonAdvancedTable.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. <!-- Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <script setup lang="ts">
  3. import {
  4. useInfiniteScroll,
  5. useLocalStorage,
  6. whenever,
  7. useEventListener,
  8. } from '@vueuse/core'
  9. import { delay, merge } from 'lodash-es'
  10. import {
  11. computed,
  12. nextTick,
  13. onMounted,
  14. ref,
  15. toRef,
  16. useTemplateRef,
  17. watch,
  18. type Ref,
  19. } from 'vue'
  20. import ObjectAttributeContent from '#shared/components/ObjectAttributes/ObjectAttribute.vue'
  21. import { useDebouncedLoading } from '#shared/composables/useDebouncedLoading.ts'
  22. import { useObjectAttributes } from '#shared/entities/object-attributes/composables/useObjectAttributes.ts'
  23. import type { ObjectAttribute } from '#shared/entities/object-attributes/types/store.ts'
  24. import type { TicketById } from '#shared/entities/ticket/types.ts'
  25. import {
  26. EnumObjectManagerObjects,
  27. EnumOrderDirection,
  28. } from '#shared/graphql/types.ts'
  29. import { i18n } from '#shared/i18n.ts'
  30. import type { ObjectLike } from '#shared/types/utils.ts'
  31. import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
  32. import CommonTableRowsSkeleton from '#desktop/components/CommonTable/Skeleton/CommonTableRowsSkeleton.vue'
  33. import TableCaption from '#desktop/components/CommonTable/TableCaption.vue'
  34. import { useTableCheckboxes } from './composables/useTableCheckboxes.ts'
  35. import HeaderResizeLine from './HeaderResizeLine.vue'
  36. import TableRow from './TableRow.vue'
  37. import TableRowGroupBy from './TableRowGroupBy.vue'
  38. import {
  39. MINIMUM_COLUMN_WIDTH,
  40. MINIMUM_TABLE_WIDTH,
  41. type AdvancedTableProps,
  42. type TableAdvancedItem,
  43. type TableAttribute,
  44. } from './types.ts'
  45. // TODO: Clarify defaults.
  46. const props = withDefaults(defineProps<AdvancedTableProps>(), {
  47. maxItems: 1000,
  48. reachedScrollTop: true,
  49. })
  50. const emit = defineEmits<{
  51. 'click-row': [TableAdvancedItem]
  52. sort: [string, EnumOrderDirection]
  53. }>()
  54. // Styling
  55. const cellAlignmentClasses = {
  56. right: 'ltr:text-right rtl:text-left',
  57. center: 'text-center',
  58. left: 'ltr:text-left rtl:text-right',
  59. }
  60. const { attributesLookup: objectAttributesLookup } = props.object
  61. ? useObjectAttributes(props.object)
  62. : { attributesLookup: null }
  63. const localAttributesLookup = computed(() => {
  64. const lookup: Map<string, TableAttribute> = new Map()
  65. props.attributes?.forEach((attribute) =>
  66. lookup.set(attribute.name, attribute),
  67. )
  68. return lookup
  69. })
  70. const findAttribute = <T,>(headerName: string, lookup: Map<string, T>) => {
  71. return (
  72. lookup?.get(headerName) ||
  73. lookup?.get(`${headerName}_id`) ||
  74. lookup?.get(`${headerName}_ids`)
  75. )
  76. }
  77. const localHeaders = computed(() => {
  78. if (!props.groupBy) return props.headers
  79. return props.headers.filter((header) => header !== props.groupBy)
  80. })
  81. // TODO: bulk checkbox fixed in template
  82. const rightAlignedDataTypes = ['date', 'datetime', 'integer']
  83. const tableAttributes = computed(() => {
  84. const table: TableAttribute[] = []
  85. // Process each header
  86. localHeaders.value.forEach((headerName) => {
  87. // Try to find matching attribute from both sources
  88. const localAttribute = findAttribute(
  89. headerName,
  90. localAttributesLookup.value,
  91. )
  92. const objectAttribute = objectAttributesLookup?.value
  93. ? findAttribute(headerName, objectAttributesLookup.value)
  94. : null
  95. // Skip if no attribute definition found
  96. if (!localAttribute && !objectAttribute) return
  97. const attributeExtension = props.attributeExtensions?.[headerName]
  98. // Convert ObjectAttribute to TableAttribute structure if it exists
  99. const mergedAttribute = objectAttribute
  100. ? (merge(
  101. {
  102. name: objectAttribute.name,
  103. label: objectAttribute.display,
  104. dataType: objectAttribute.dataType,
  105. dataOption: objectAttribute.dataOption || {},
  106. headerPreferences: {},
  107. columnPreferences: {},
  108. },
  109. attributeExtension,
  110. ) as TableAttribute)
  111. : (localAttribute as TableAttribute)
  112. // Set default alignment for right-aligned data types.
  113. if (
  114. rightAlignedDataTypes.includes(mergedAttribute.dataType) &&
  115. !mergedAttribute.columnPreferences.alignContent
  116. ) {
  117. mergedAttribute.columnPreferences.alignContent = 'right'
  118. }
  119. table.push(mergedAttribute)
  120. })
  121. return table
  122. })
  123. const tableColumnLength = computed(() => {
  124. return tableAttributes.value.length + (props.actions ? 1 : 0)
  125. })
  126. const tableElement = useTemplateRef('table')
  127. // FIXME: Temporary initialization to avoid empty reference.
  128. let headerWidthsRelativeStorage: Ref<Record<string, number>> = ref({})
  129. const setHeaderWidths = (reset?: boolean) => {
  130. if (!tableElement.value || !tableElement.value.parentElement) return
  131. const availableWidth = tableElement.value.parentElement.clientWidth
  132. const tableWidth =
  133. availableWidth < MINIMUM_TABLE_WIDTH ? MINIMUM_TABLE_WIDTH : availableWidth
  134. tableElement.value.style.width = `${tableWidth}px`
  135. tableAttributes.value.forEach((tableAttribute) => {
  136. const header = document.getElementById(`${tableAttribute.name}-header`)
  137. if (!header) return
  138. if (reset) {
  139. if (tableAttribute.headerPreferences.displayWidth)
  140. header.style.width = `${tableAttribute.headerPreferences.displayWidth}px`
  141. else header.style.width = '' // reflow
  142. return
  143. }
  144. const headerWidthRelative =
  145. headerWidthsRelativeStorage.value[tableAttribute.name]
  146. const headerWidth =
  147. tableAttribute.headerPreferences.displayWidth ??
  148. Math.max(MINIMUM_COLUMN_WIDTH, headerWidthRelative * tableWidth)
  149. header.style.width = `${headerWidth}px`
  150. })
  151. }
  152. const storeHeaderWidths = (headerWidths: Record<string, number>) => {
  153. const headerWidthsRelative = Object.keys(headerWidths).reduce(
  154. (headerWidthsRelative, headerName) => {
  155. if (!tableElement.value) return headerWidthsRelative
  156. headerWidthsRelative[headerName] =
  157. headerWidths[headerName] / tableElement.value.clientWidth
  158. return headerWidthsRelative
  159. },
  160. {} as Record<string, number>,
  161. )
  162. headerWidthsRelativeStorage.value = headerWidthsRelative
  163. }
  164. const calculateHeaderWidths = () => {
  165. const headerWidths: Record<string, number> = {}
  166. tableAttributes.value.forEach((tableAttribute) => {
  167. const headerWidth = document.getElementById(
  168. `${tableAttribute.name}-header`,
  169. )?.clientWidth
  170. if (!headerWidth) return
  171. headerWidths[tableAttribute.name] = headerWidth
  172. })
  173. storeHeaderWidths(headerWidths)
  174. }
  175. const initializeHeaderWidths = (storageKeyId?: string) => {
  176. if (storageKeyId) {
  177. // FIXME: This is needed because storage key as a reactive value is unsupported.
  178. // eslint-disable-next-line vue/no-ref-as-operand
  179. headerWidthsRelativeStorage = useLocalStorage<Record<string, number>>(
  180. storageKeyId,
  181. {},
  182. )
  183. }
  184. nextTick(() => {
  185. setHeaderWidths()
  186. calculateHeaderWidths()
  187. })
  188. }
  189. const resetHeaderWidths = () => {
  190. setHeaderWidths(true)
  191. delay(calculateHeaderWidths, 500)
  192. }
  193. watch(() => props.storageKeyId, initializeHeaderWidths)
  194. watch(
  195. () => props.headers,
  196. () => initializeHeaderWidths,
  197. )
  198. onMounted(() => {
  199. if (!props.storageKeyId) return
  200. initializeHeaderWidths(props.storageKeyId)
  201. })
  202. useEventListener('resize', () => initializeHeaderWidths())
  203. const getTooltipText = (
  204. item: TableAdvancedItem,
  205. tableAttribute: TableAttribute,
  206. ) => {
  207. return tableAttribute.headerPreferences.truncate
  208. ? item[tableAttribute.name]
  209. : undefined
  210. }
  211. const checkedRows = defineModel<Array<TableAdvancedItem>>('checkedRows', {
  212. required: false,
  213. default: (props: AdvancedTableProps) =>
  214. props.items.filter((item) => item.checked), // is not reactive by default and making it reactive causes other issues.
  215. })
  216. const {
  217. hasCheckboxId,
  218. allCheckboxRowsSelected,
  219. selectAllRowCheckboxes,
  220. handleCheckboxUpdate,
  221. } = useTableCheckboxes(checkedRows, toRef(props, 'items'))
  222. const rowHandlers = computed(() =>
  223. props.onClickRow || props.hasCheckboxColumn
  224. ? {
  225. 'click-row': (event: TableAdvancedItem) => {
  226. if (props.onClickRow) props.onClickRow(event)
  227. if (props.hasCheckboxColumn) handleCheckboxUpdate(event)
  228. },
  229. }
  230. : {},
  231. )
  232. const localItems = computed(() => {
  233. return props.items.slice(0, props.maxItems)
  234. })
  235. const remainingItems = computed(() => {
  236. const itemCount =
  237. props.totalItems >= props.maxItems ? props.maxItems : props.totalItems
  238. return itemCount - localItems.value.length
  239. })
  240. const sort = (column: string) => {
  241. const newDirection =
  242. props.orderBy === column &&
  243. props.orderDirection === EnumOrderDirection.Ascending
  244. ? EnumOrderDirection.Descending
  245. : EnumOrderDirection.Ascending
  246. emit('sort', column, newDirection)
  247. }
  248. const isSorted = (column: string) => props.orderBy === column
  249. const sortIcon = computed(() =>
  250. props.orderDirection === EnumOrderDirection.Ascending
  251. ? 'arrow-up-short'
  252. : 'arrow-down-short',
  253. )
  254. let currentGroupByValueIndex = -1
  255. const groupByAttribute = computed(() => {
  256. if (!props.groupBy) return null
  257. // Try to find matching attribute from both sources
  258. const localAttribute = findAttribute(
  259. props.groupBy,
  260. localAttributesLookup.value,
  261. )
  262. const objectAttribute = objectAttributesLookup?.value
  263. ? findAttribute(props.groupBy, objectAttributesLookup.value)
  264. : null
  265. return (localAttribute || objectAttribute) as TableAttribute
  266. })
  267. const groupByAttributeItemName = computed(() => {
  268. if (!groupByAttribute.value) return
  269. return (
  270. groupByAttribute.value.dataOption?.belongs_to || groupByAttribute.value.name
  271. )
  272. })
  273. const groupByRowCounts = computed(() => {
  274. if (!groupByAttribute.value || !groupByAttributeItemName.value) return
  275. const name = groupByAttributeItemName.value
  276. const isRelation = Boolean(groupByAttribute.value.dataOption?.relation)
  277. let groupByValueIndex = 0
  278. let lastValue: string | number
  279. return localItems.value.reduce((groupByRowIds: string[][], item) => {
  280. const value = (isRelation ? (item[name] as ObjectLike).id : item[name]) as
  281. | string
  282. | number
  283. if (lastValue && value !== lastValue) {
  284. groupByValueIndex += 1
  285. }
  286. groupByRowIds[groupByValueIndex] ||= []
  287. groupByRowIds[groupByValueIndex].push(item.id)
  288. lastValue = value
  289. return groupByRowIds
  290. }, [])
  291. })
  292. const showGroupByRow = (item: TableAdvancedItem) => {
  293. if (!groupByAttribute.value || !groupByRowCounts.value) return false
  294. // Reset the current group by value when it's the first item again.
  295. if (item.id === localItems.value[0].id) {
  296. currentGroupByValueIndex = -1
  297. }
  298. const show = Boolean(
  299. currentGroupByValueIndex === -1 ||
  300. !groupByRowCounts.value[currentGroupByValueIndex].includes(item.id),
  301. )
  302. // Remember current group index, when it should be shown.
  303. if (show) {
  304. currentGroupByValueIndex += 1
  305. }
  306. return show
  307. }
  308. const hasLoadedMore = ref(false)
  309. const { isLoading } = useInfiniteScroll(
  310. toRef(props, 'scrollContainer'),
  311. async () => {
  312. hasLoadedMore.value = true
  313. await props.onLoadMore?.()
  314. },
  315. {
  316. distance: 100,
  317. canLoadMore: () => remainingItems.value > 0,
  318. },
  319. )
  320. const { debouncedLoading } = useDebouncedLoading({ isLoading })
  321. whenever(
  322. isLoading,
  323. () => {
  324. hasLoadedMore.value = true
  325. },
  326. { once: true },
  327. )
  328. const endOfListMessage = computed(() => {
  329. if (!hasLoadedMore.value) return ''
  330. if (remainingItems.value !== 0) return ''
  331. if (props.totalItems > props.maxItems) {
  332. return i18n.t(
  333. 'You reached the table limit of %s tickets (%s remaining).',
  334. props.maxItems,
  335. props.totalItems - localItems.value.length,
  336. )
  337. }
  338. return i18n.t("You don't have more tickets to load.")
  339. })
  340. const getLinkColorClasses = (item: TableAdvancedItem) => {
  341. if (props.object !== EnumObjectManagerObjects.Ticket) return ''
  342. switch ((item as TicketById).priority?.uiColor) {
  343. case 'high-priority':
  344. return 'text-red-500 dark:text-red-500'
  345. case 'low-priority':
  346. return 'text-stone-200 dark:text-neutral-500'
  347. default:
  348. return ''
  349. }
  350. }
  351. </script>
  352. <template>
  353. <table v-bind="$attrs" ref="table" class="relative table-fixed pb-3">
  354. <TableCaption :show="showCaption">{{ caption }}</TableCaption>
  355. <thead
  356. class="sticky top-0 z-10 bg-neutral-50 dark:bg-gray-500"
  357. :class="{ 'border-shadow-b': !reachedScrollTop }"
  358. >
  359. <tr>
  360. <th
  361. v-for="(tableAttribute, index) in tableAttributes"
  362. :id="`${tableAttribute.name}-header`"
  363. :key="tableAttribute.name"
  364. class="relative h-10 p-2.5 text-xs"
  365. :class="[
  366. tableAttribute.headerPreferences.headerClass,
  367. cellAlignmentClasses[
  368. tableAttribute.columnPreferences.alignContent || 'left'
  369. ],
  370. ]"
  371. >
  372. <!-- TODO: Implement with bulk edit -->
  373. <FormKit
  374. v-if="hasCheckboxColumn && tableAttribute.name === 'checkbox'"
  375. name="checkbox-all-rows"
  376. :aria-label="
  377. allCheckboxRowsSelected
  378. ? $t('Deselect all entries')
  379. : $t('Select all entries')
  380. "
  381. type="checkbox"
  382. :model-value="allCheckboxRowsSelected"
  383. @update:model-value="selectAllRowCheckboxes"
  384. />
  385. <template v-else>
  386. <slot
  387. :name="`column-header-${tableAttribute.name}`"
  388. :attribute="tableAttribute"
  389. >
  390. <CommonLabel
  391. class="-:font-normal -:text-gray-100 -:dark:text-neutral-400 block select-none truncate"
  392. :class="[
  393. cellAlignmentClasses[
  394. tableAttribute.columnPreferences.alignContent || 'left'
  395. ],
  396. tableAttribute.headerPreferences.labelClass || '',
  397. {
  398. 'sr-only': tableAttribute.headerPreferences.hideLabel,
  399. 'text-black dark:text-white': isSorted(tableAttribute.name),
  400. 'hover:cursor-pointer hover:text-black focus-visible:rounded-sm focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-blue-800 dark:hover:text-white':
  401. !tableAttribute.headerPreferences.noSorting,
  402. },
  403. ]"
  404. :aria-label="
  405. orderDirection === EnumOrderDirection.Ascending
  406. ? $t('Sorted ascending')
  407. : $t('Sorted descending')
  408. "
  409. :role="
  410. tableAttribute.headerPreferences.noSorting
  411. ? undefined
  412. : 'button'
  413. "
  414. :tabindex="
  415. tableAttribute.headerPreferences.noSorting ? undefined : '0'
  416. "
  417. size="small"
  418. @click="
  419. tableAttribute.headerPreferences.noSorting
  420. ? undefined
  421. : sort(tableAttribute.name)
  422. "
  423. @keydown.enter.prevent="
  424. tableAttribute.headerPreferences.noSorting
  425. ? undefined
  426. : sort(tableAttribute.name)
  427. "
  428. @keydown.space.prevent="
  429. tableAttribute.headerPreferences.noSorting
  430. ? undefined
  431. : sort(tableAttribute.name)
  432. "
  433. >
  434. {{
  435. $t(
  436. tableAttribute.label,
  437. ...(tableAttribute.labelPlaceholder || []),
  438. )
  439. }}
  440. <CommonIcon
  441. v-if="
  442. !tableAttribute.headerPreferences.noSorting &&
  443. isSorted(tableAttribute.name)
  444. "
  445. class="inline text-blue-800"
  446. :name="sortIcon"
  447. size="xs"
  448. decorative
  449. />
  450. </CommonLabel>
  451. </slot>
  452. </template>
  453. <slot
  454. :name="`header-suffix-${tableAttribute.name}`"
  455. :item="tableAttribute"
  456. />
  457. <HeaderResizeLine
  458. v-if="
  459. !tableAttribute.headerPreferences.noResize &&
  460. index !== tableAttributes.length - 1
  461. "
  462. @resize="calculateHeaderWidths"
  463. @reset="resetHeaderWidths"
  464. />
  465. </th>
  466. <th v-if="actions" class="h-10 w-0 p-2.5 text-center">
  467. <CommonLabel
  468. class="font-normal text-stone-200 dark:text-neutral-500"
  469. size="small"
  470. >{{ $t('Actions') }}
  471. </CommonLabel>
  472. </th>
  473. </tr>
  474. </thead>
  475. <!-- :TODO tabindex should be -1 re-evaluate when we work on bulk action with checkbox -->
  476. <!-- SR should not be able to focus the row but each action node -->
  477. <tbody>
  478. <template v-for="item in localItems" :key="item.id">
  479. <TableRowGroupBy
  480. v-if="groupByAttribute && showGroupByRow(item)"
  481. ref=""
  482. :item="item"
  483. :attribute="groupByAttribute"
  484. :table-column-length="tableColumnLength"
  485. :group-by-value-index="currentGroupByValueIndex"
  486. :group-by-row-counts="groupByRowCounts"
  487. :remaining-items="remainingItems"
  488. />
  489. <TableRow
  490. :item="item"
  491. :is-row-selected="
  492. !hasCheckboxColumn && item.id === props.selectedRowId
  493. "
  494. tabindex="-1"
  495. :has-checkbox="hasCheckboxColumn"
  496. :with-even-stripes="!!groupByAttribute"
  497. v-on="rowHandlers"
  498. >
  499. <template #default="{ isRowSelected }">
  500. <td
  501. v-for="tableAttribute in tableAttributes"
  502. :key="`${item.id}-${tableAttribute.name}`"
  503. class="h-10 p-2.5 text-sm"
  504. :class="[
  505. cellAlignmentClasses[
  506. tableAttribute.columnPreferences.alignContent || 'left'
  507. ],
  508. {
  509. 'max-w-32 truncate text-black dark:text-white':
  510. tableAttribute.headerPreferences.truncate,
  511. },
  512. ]"
  513. >
  514. <FormKit
  515. v-if="hasCheckboxColumn && tableAttribute.name === 'checkbox'"
  516. :key="`checkbox-${item.id}-${tableAttribute.name}`"
  517. :name="`checkbox-${item.id}`"
  518. :aria-label="
  519. hasCheckboxId(item.id)
  520. ? $t('Deselect this entry')
  521. : $t('Select this entry')
  522. "
  523. type="checkbox"
  524. :alternative-background="true"
  525. :classes="{
  526. decorator:
  527. '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',
  528. decoratorIcon:
  529. 'group-active:formkit-checked:text-white group-hover:formkit-checked:text-black group-hover:formkit-checked:dark:text-white',
  530. }"
  531. :disabled="!!item.disabled"
  532. :model-value="hasCheckboxId(item.id)"
  533. @click="handleCheckboxUpdate(item)"
  534. @keydown.enter="handleCheckboxUpdate(item)"
  535. @keydown.space="handleCheckboxUpdate(item)"
  536. />
  537. <template v-else>
  538. <slot
  539. :name="`column-cell-${tableAttribute.name}`"
  540. :item="item"
  541. :is-row-selected="isRowSelected"
  542. :attribute="tableAttribute"
  543. >
  544. <CommonLink
  545. v-if="tableAttribute.columnPreferences.link"
  546. v-tooltip.truncate="getTooltipText(item, tableAttribute)"
  547. :link="
  548. tableAttribute.columnPreferences.link.getLink(
  549. item,
  550. tableAttribute,
  551. )
  552. "
  553. :open-in-new-tab="
  554. tableAttribute.columnPreferences.link.openInNewTab
  555. "
  556. :internal="tableAttribute.columnPreferences.link.internal"
  557. :class="[
  558. {
  559. 'text-black dark:text-white': isRowSelected,
  560. },
  561. getLinkColorClasses(item),
  562. ]"
  563. class="block truncate text-sm hover:no-underline group-hover:text-black group-focus-visible:text-white group-active:text-white group-hover:dark:text-white"
  564. @click.stop
  565. @keydown.stop
  566. >
  567. <ObjectAttributeContent
  568. mode="table"
  569. :attribute="tableAttribute as unknown as ObjectAttribute"
  570. :object="item"
  571. />
  572. </CommonLink>
  573. <CommonLabel
  574. v-else
  575. v-tooltip.truncate="getTooltipText(item, tableAttribute)"
  576. class="-:text-gray-100 -:dark:text-neutral-400 block truncate group-hover:text-black group-focus-visible:text-white group-active:text-white group-hover:dark:text-white"
  577. :class="[
  578. {
  579. 'text-black dark:text-white': isRowSelected,
  580. },
  581. ]"
  582. >
  583. <ObjectAttributeContent
  584. mode="table"
  585. :attribute="tableAttribute as unknown as ObjectAttribute"
  586. :object="item"
  587. />
  588. </CommonLabel>
  589. </slot>
  590. <slot
  591. :name="`item-suffix-${tableAttribute.name}`"
  592. :item="item"
  593. />
  594. </template>
  595. </td>
  596. <td v-if="actions" class="h-10 p-2.5 text-center">
  597. <slot name="actions" v-bind="{ actions, item }">
  598. <CommonActionMenu
  599. class="flex items-center justify-center"
  600. :actions="actions"
  601. :entity="item"
  602. button-size="medium"
  603. />
  604. </slot>
  605. </td>
  606. </template>
  607. </TableRow>
  608. </template>
  609. <Transition leave-active-class="absolute">
  610. <div
  611. v-if="debouncedLoading"
  612. :class="{ 'pt-10': localItems.length % 2 !== 0 }"
  613. class="absolute w-full pb-4"
  614. >
  615. <CommonTableRowsSkeleton :rows="3" />
  616. </div>
  617. </Transition>
  618. </tbody>
  619. </table>
  620. <CommonLabel
  621. v-if="endOfListMessage"
  622. class="py-2.5 text-stone-200 dark:text-neutral-500"
  623. size="small"
  624. >
  625. {{ endOfListMessage }}
  626. </CommonLabel>
  627. </template>
  628. <style scoped>
  629. [data-theme='dark'] .border-shadow-b {
  630. box-shadow: 0 1px 0 0 theme('colors.gray.900');
  631. }
  632. .border-shadow-b {
  633. box-shadow: 0 1px 0 0 theme('colors.neutral.100');
  634. }
  635. </style>