CommonAdvancedTable.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  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 { onBeforeRouteUpdate } from 'vue-router'
  21. import ObjectAttributeContent from '#shared/components/ObjectAttributes/ObjectAttribute.vue'
  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. let shouldReset = reset
  136. if (
  137. tableAttributes.value.length !==
  138. Object.keys(headerWidthsRelativeStorage.value).length
  139. )
  140. shouldReset = true
  141. tableAttributes.value.forEach((tableAttribute) => {
  142. const header = document.getElementById(`${tableAttribute.name}-header`)
  143. if (!header) return
  144. if (shouldReset) {
  145. if (tableAttribute.headerPreferences.displayWidth)
  146. header.style.width = `${tableAttribute.headerPreferences.displayWidth}px`
  147. else header.style.width = '' // reflow
  148. return
  149. }
  150. const headerWidthRelative =
  151. headerWidthsRelativeStorage.value[tableAttribute.name]
  152. const headerWidth =
  153. tableAttribute.headerPreferences.displayWidth ??
  154. Math.max(MINIMUM_COLUMN_WIDTH, headerWidthRelative * tableWidth)
  155. header.style.width = `${headerWidth}px`
  156. })
  157. }
  158. const storeHeaderWidths = (headerWidths: Record<string, number>) => {
  159. const headerWidthsRelative = Object.keys(headerWidths).reduce(
  160. (headerWidthsRelative, headerName) => {
  161. if (!tableElement.value) return headerWidthsRelative
  162. headerWidthsRelative[headerName] =
  163. headerWidths[headerName] / tableElement.value.clientWidth
  164. return headerWidthsRelative
  165. },
  166. {} as Record<string, number>,
  167. )
  168. headerWidthsRelativeStorage.value = headerWidthsRelative
  169. }
  170. const calculateHeaderWidths = () => {
  171. const headerWidths: Record<string, number> = {}
  172. tableAttributes.value.forEach((tableAttribute) => {
  173. const headerWidth = document.getElementById(
  174. `${tableAttribute.name}-header`,
  175. )?.clientWidth
  176. if (!headerWidth) return
  177. headerWidths[tableAttribute.name] = headerWidth
  178. })
  179. storeHeaderWidths(headerWidths)
  180. }
  181. const initializeHeaderWidths = (storageKeyId?: string) => {
  182. if (storageKeyId) {
  183. // FIXME: This is needed because storage key as a reactive value is unsupported.
  184. // eslint-disable-next-line vue/no-ref-as-operand
  185. headerWidthsRelativeStorage = useLocalStorage<Record<string, number>>(
  186. storageKeyId,
  187. {},
  188. )
  189. }
  190. nextTick(() => {
  191. setHeaderWidths()
  192. delay(calculateHeaderWidths, 500)
  193. })
  194. }
  195. const resetHeaderWidths = () => {
  196. setHeaderWidths(true)
  197. delay(calculateHeaderWidths, 500)
  198. }
  199. watch(() => props.storageKeyId, initializeHeaderWidths)
  200. watch(localHeaders, () => {
  201. initializeHeaderWidths()
  202. })
  203. onMounted(() => {
  204. if (!props.storageKeyId) return
  205. initializeHeaderWidths(props.storageKeyId)
  206. })
  207. useEventListener('resize', () => initializeHeaderWidths())
  208. const getTooltipText = (
  209. item: TableAdvancedItem,
  210. tableAttribute: TableAttribute,
  211. ) => {
  212. return tableAttribute.headerPreferences.truncate
  213. ? item[tableAttribute.name]
  214. : undefined
  215. }
  216. const checkedRows = defineModel<Array<TableAdvancedItem>>('checkedRows', {
  217. required: false,
  218. default: (props: AdvancedTableProps) =>
  219. props.items.filter((item) => item.checked), // is not reactive by default and making it reactive causes other issues.
  220. })
  221. const {
  222. hasCheckboxId,
  223. allCheckboxRowsSelected,
  224. selectAllRowCheckboxes,
  225. handleCheckboxUpdate,
  226. } = useTableCheckboxes(checkedRows, toRef(props, 'items'))
  227. const rowHandlers = computed(() =>
  228. props.onClickRow || props.hasCheckboxColumn
  229. ? {
  230. 'click-row': (event: TableAdvancedItem) => {
  231. if (props.onClickRow) props.onClickRow(event)
  232. if (props.hasCheckboxColumn) handleCheckboxUpdate(event)
  233. },
  234. }
  235. : {},
  236. )
  237. const localItems = computed(() => {
  238. return props.items.slice(0, props.maxItems)
  239. })
  240. const remainingItems = computed(() => {
  241. const itemCount =
  242. props.totalItems >= props.maxItems ? props.maxItems : props.totalItems
  243. return itemCount - localItems.value.length
  244. })
  245. const sort = (column: string) => {
  246. const newDirection =
  247. props.orderBy === column &&
  248. props.orderDirection === EnumOrderDirection.Ascending
  249. ? EnumOrderDirection.Descending
  250. : EnumOrderDirection.Ascending
  251. emit('sort', column, newDirection)
  252. }
  253. const isSorted = (column: string) => props.orderBy === column
  254. const sortIcon = computed(() =>
  255. props.orderDirection === EnumOrderDirection.Ascending
  256. ? 'arrow-up-short'
  257. : 'arrow-down-short',
  258. )
  259. let currentGroupByValueIndex = -1
  260. const groupByAttribute = computed(() => {
  261. if (!props.groupBy) return null
  262. // Try to find matching attribute from both sources
  263. const localAttribute = findAttribute(
  264. props.groupBy,
  265. localAttributesLookup.value,
  266. )
  267. const objectAttribute = objectAttributesLookup?.value
  268. ? findAttribute(props.groupBy, objectAttributesLookup.value)
  269. : null
  270. return (localAttribute || objectAttribute) as TableAttribute
  271. })
  272. const groupByAttributeItemName = computed(() => {
  273. if (!groupByAttribute.value) return
  274. return (
  275. groupByAttribute.value.dataOption?.belongs_to || groupByAttribute.value.name
  276. )
  277. })
  278. const groupByRowCounts = computed(() => {
  279. if (!groupByAttribute.value || !groupByAttributeItemName.value) return
  280. const name = groupByAttributeItemName.value
  281. const isRelation = Boolean(groupByAttribute.value.dataOption?.relation)
  282. let groupByValueIndex = 0
  283. let lastValue: string | number
  284. return localItems.value.reduce((groupByRowIds: string[][], item) => {
  285. const value = (
  286. isRelation && item[name] ? (item[name] as ObjectLike).id : item[name]
  287. ) as string | number
  288. if (lastValue && value !== lastValue) {
  289. groupByValueIndex += 1
  290. }
  291. groupByRowIds[groupByValueIndex] ||= []
  292. groupByRowIds[groupByValueIndex].push(item.id)
  293. lastValue = value
  294. return groupByRowIds
  295. }, [])
  296. })
  297. const groupIndexByRowId = (groupIndex: number, rowId: string) =>
  298. groupByRowCounts.value?.[groupIndex]?.findIndex((id) => id === rowId) || 0
  299. const showGroupByRow = (item: TableAdvancedItem) => {
  300. if (!groupByAttribute.value || !groupByRowCounts.value) return false
  301. // Reset the current group by value when it's the first item again.
  302. if (item.id === localItems.value[0].id) {
  303. currentGroupByValueIndex = -1
  304. }
  305. const show = Boolean(
  306. currentGroupByValueIndex === -1 ||
  307. !groupByRowCounts.value[currentGroupByValueIndex].includes(item.id),
  308. )
  309. // Remember current group index, when it should be shown.
  310. if (show) {
  311. currentGroupByValueIndex += 1
  312. }
  313. return show
  314. }
  315. const hasLoadedMore = ref(false)
  316. onBeforeRouteUpdate(() => {
  317. hasLoadedMore.value = false
  318. })
  319. const { isLoading } = useInfiniteScroll(
  320. toRef(props, 'scrollContainer'),
  321. async () => {
  322. hasLoadedMore.value = true
  323. await props.onLoadMore?.()
  324. },
  325. {
  326. distance: 100,
  327. canLoadMore: () => remainingItems.value > 0,
  328. eventListenerOptions: {
  329. passive: true,
  330. },
  331. },
  332. )
  333. whenever(
  334. isLoading,
  335. () => {
  336. hasLoadedMore.value = true
  337. },
  338. { once: true },
  339. )
  340. const endOfListMessage = computed(() => {
  341. if (!hasLoadedMore.value) return ''
  342. if (remainingItems.value !== 0) return ''
  343. return props.totalItems > props.maxItems
  344. ? i18n.t(
  345. 'You reached the table limit of %s tickets (%s remaining).',
  346. props.maxItems,
  347. props.totalItems - localItems.value.length,
  348. )
  349. : i18n.t("You don't have more tickets to load.")
  350. })
  351. const getLinkColorClasses = (item: TableAdvancedItem) => {
  352. if (props.object !== EnumObjectManagerObjects.Ticket) return ''
  353. switch ((item as TicketById).priority?.uiColor) {
  354. case 'high-priority':
  355. return 'text-red-500 dark:text-red-500'
  356. case 'low-priority':
  357. return 'text-stone-200 dark:text-neutral-500'
  358. default:
  359. return ''
  360. }
  361. }
  362. </script>
  363. <template>
  364. <table v-bind="$attrs" ref="table" class="relative table-fixed pb-3">
  365. <TableCaption :show="showCaption">{{ caption }}</TableCaption>
  366. <thead
  367. class="sticky top-0 z-10 bg-neutral-50 dark:bg-gray-500"
  368. :class="{ 'border-shadow-b': !reachedScrollTop }"
  369. >
  370. <tr>
  371. <th
  372. v-for="(tableAttribute, index) in tableAttributes"
  373. :id="`${tableAttribute.name}-header`"
  374. :key="tableAttribute.name"
  375. class="relative h-10 p-2.5 text-xs"
  376. :class="[
  377. tableAttribute.headerPreferences.headerClass,
  378. cellAlignmentClasses[
  379. tableAttribute.columnPreferences.alignContent || 'left'
  380. ],
  381. ]"
  382. >
  383. <!-- TODO: Implement with bulk edit -->
  384. <FormKit
  385. v-if="hasCheckboxColumn && tableAttribute.name === 'checkbox'"
  386. name="checkbox-all-rows"
  387. :aria-label="
  388. allCheckboxRowsSelected
  389. ? $t('Deselect all entries')
  390. : $t('Select all entries')
  391. "
  392. type="checkbox"
  393. :model-value="allCheckboxRowsSelected"
  394. @update:model-value="selectAllRowCheckboxes"
  395. />
  396. <template v-else>
  397. <slot
  398. :name="`column-header-${tableAttribute.name}`"
  399. :attribute="tableAttribute"
  400. >
  401. <CommonLabel
  402. class="-:font-normal -:text-gray-100 -:dark:text-neutral-400 block select-none truncate"
  403. :class="[
  404. cellAlignmentClasses[
  405. tableAttribute.columnPreferences.alignContent || 'left'
  406. ],
  407. tableAttribute.headerPreferences.labelClass || '',
  408. {
  409. 'sr-only': tableAttribute.headerPreferences.hideLabel,
  410. 'text-black dark:text-white': isSorted(tableAttribute.name),
  411. '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':
  412. !tableAttribute.headerPreferences.noSorting,
  413. },
  414. ]"
  415. :aria-label="
  416. orderDirection === EnumOrderDirection.Ascending
  417. ? $t('Sorted ascending')
  418. : $t('Sorted descending')
  419. "
  420. :role="
  421. tableAttribute.headerPreferences.noSorting
  422. ? undefined
  423. : 'button'
  424. "
  425. :tabindex="
  426. tableAttribute.headerPreferences.noSorting ? undefined : '0'
  427. "
  428. size="small"
  429. @click="
  430. tableAttribute.headerPreferences.noSorting
  431. ? undefined
  432. : sort(tableAttribute.name)
  433. "
  434. @keydown.enter.prevent="
  435. tableAttribute.headerPreferences.noSorting
  436. ? undefined
  437. : sort(tableAttribute.name)
  438. "
  439. @keydown.space.prevent="
  440. tableAttribute.headerPreferences.noSorting
  441. ? undefined
  442. : sort(tableAttribute.name)
  443. "
  444. >
  445. {{
  446. $t(
  447. tableAttribute.label,
  448. ...(tableAttribute.labelPlaceholder || []),
  449. )
  450. }}
  451. <CommonIcon
  452. v-if="
  453. !tableAttribute.headerPreferences.noSorting &&
  454. isSorted(tableAttribute.name)
  455. "
  456. class="inline text-blue-800"
  457. :name="sortIcon"
  458. size="xs"
  459. decorative
  460. />
  461. </CommonLabel>
  462. </slot>
  463. </template>
  464. <slot
  465. :name="`header-suffix-${tableAttribute.name}`"
  466. :item="tableAttribute"
  467. />
  468. <HeaderResizeLine
  469. v-if="
  470. !tableAttribute.headerPreferences.noResize &&
  471. index !== tableAttributes.length - 1
  472. "
  473. @resize="calculateHeaderWidths"
  474. @reset="resetHeaderWidths"
  475. />
  476. </th>
  477. <th v-if="actions" class="h-10 w-0 p-2.5 text-center">
  478. <CommonLabel
  479. class="font-normal text-stone-200 dark:text-neutral-500"
  480. size="small"
  481. >{{ $t('Actions') }}
  482. </CommonLabel>
  483. </th>
  484. </tr>
  485. </thead>
  486. <!-- :TODO tabindex should be -1 re-evaluate when we work on bulk action with checkbox -->
  487. <!-- SR should not be able to focus the row but each action node -->
  488. <tbody
  489. class="relative"
  490. :inert="isSorting"
  491. :class="{
  492. 'opacity-50 before:absolute before:z-20 before:h-full before:w-full':
  493. isSorting,
  494. }"
  495. >
  496. <template v-for="item in localItems" :key="item.id">
  497. <TableRowGroupBy
  498. v-if="groupByAttribute && showGroupByRow(item)"
  499. ref=""
  500. :item="item"
  501. :attribute="groupByAttribute"
  502. :table-column-length="tableColumnLength"
  503. :group-by-value-index="currentGroupByValueIndex"
  504. :group-by-row-counts="groupByRowCounts"
  505. :remaining-items="remainingItems"
  506. />
  507. <TableRow
  508. :item="item"
  509. :is-row-selected="
  510. !hasCheckboxColumn && item.id === props.selectedRowId
  511. "
  512. tabindex="-1"
  513. :has-checkbox="hasCheckboxColumn"
  514. :no-auto-striping="!!groupByAttribute"
  515. :is-striped="
  516. !!groupByAttribute &&
  517. groupIndexByRowId(currentGroupByValueIndex, item.id) % 2 === 0
  518. "
  519. v-on="rowHandlers"
  520. >
  521. <template #default="{ isRowSelected }">
  522. <td
  523. v-for="tableAttribute in tableAttributes"
  524. :key="`${item.id}-${tableAttribute.name}`"
  525. class="h-10 p-2.5 text-sm"
  526. :class="[
  527. cellAlignmentClasses[
  528. tableAttribute.columnPreferences.alignContent || 'left'
  529. ],
  530. {
  531. 'max-w-32 truncate text-black dark:text-white':
  532. tableAttribute.headerPreferences.truncate,
  533. },
  534. ]"
  535. >
  536. <FormKit
  537. v-if="hasCheckboxColumn && tableAttribute.name === 'checkbox'"
  538. :key="`checkbox-${item.id}-${tableAttribute.name}`"
  539. :name="`checkbox-${item.id}`"
  540. :aria-label="
  541. hasCheckboxId(item.id)
  542. ? $t('Deselect this entry')
  543. : $t('Select this entry')
  544. "
  545. type="checkbox"
  546. :alternative-background="true"
  547. :classes="{
  548. decorator:
  549. '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',
  550. decoratorIcon:
  551. 'group-active:formkit-checked:text-white group-hover:formkit-checked:text-black group-hover:formkit-checked:dark:text-white',
  552. }"
  553. :disabled="!!item.disabled"
  554. :model-value="hasCheckboxId(item.id)"
  555. @click="handleCheckboxUpdate(item)"
  556. @keydown.enter="handleCheckboxUpdate(item)"
  557. @keydown.space="handleCheckboxUpdate(item)"
  558. />
  559. <template v-else>
  560. <slot
  561. :name="`column-cell-${tableAttribute.name}`"
  562. :item="item"
  563. :is-row-selected="isRowSelected"
  564. :attribute="tableAttribute"
  565. >
  566. <CommonLink
  567. v-if="tableAttribute.columnPreferences.link"
  568. v-tooltip.truncate="getTooltipText(item, tableAttribute)"
  569. :link="
  570. tableAttribute.columnPreferences.link.getLink(
  571. item,
  572. tableAttribute,
  573. )
  574. "
  575. :open-in-new-tab="
  576. tableAttribute.columnPreferences.link.openInNewTab
  577. "
  578. :internal="tableAttribute.columnPreferences.link.internal"
  579. :class="[
  580. {
  581. 'text-black dark:text-white': isRowSelected,
  582. },
  583. getLinkColorClasses(item),
  584. ]"
  585. 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"
  586. @click.stop
  587. @keydown.stop
  588. >
  589. <ObjectAttributeContent
  590. mode="table"
  591. :attribute="tableAttribute as unknown as ObjectAttribute"
  592. :object="item"
  593. />
  594. </CommonLink>
  595. <CommonLabel
  596. v-else
  597. v-tooltip.truncate="getTooltipText(item, tableAttribute)"
  598. 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"
  599. :class="[
  600. {
  601. 'text-black dark:text-white': isRowSelected,
  602. },
  603. ]"
  604. >
  605. <ObjectAttributeContent
  606. mode="table"
  607. :attribute="tableAttribute as unknown as ObjectAttribute"
  608. :object="item"
  609. />
  610. </CommonLabel>
  611. </slot>
  612. <slot
  613. :name="`item-suffix-${tableAttribute.name}`"
  614. :item="item"
  615. />
  616. </template>
  617. </td>
  618. <td v-if="actions" class="h-10 p-2.5 text-center">
  619. <slot name="actions" v-bind="{ actions, item }">
  620. <CommonActionMenu
  621. class="flex items-center justify-center"
  622. :actions="actions"
  623. :entity="item"
  624. button-size="medium"
  625. />
  626. </slot>
  627. </td>
  628. </template>
  629. </TableRow>
  630. </template>
  631. <Transition leave-active-class="absolute">
  632. <div
  633. v-if="isLoading"
  634. :class="{ 'pt-10': localItems.length % 2 !== 0 }"
  635. class="absolute w-full pb-4"
  636. >
  637. <CommonTableRowsSkeleton :rows="3" />
  638. </div>
  639. </Transition>
  640. </tbody>
  641. </table>
  642. <CommonLabel
  643. v-if="endOfListMessage"
  644. class="py-2.5 text-stone-200 dark:text-neutral-500"
  645. size="small"
  646. >
  647. {{ endOfListMessage }}
  648. </CommonLabel>
  649. </template>
  650. <style scoped>
  651. [data-theme='dark'] .border-shadow-b {
  652. box-shadow: 0 1px 0 0 theme('colors.gray.900');
  653. }
  654. .border-shadow-b {
  655. box-shadow: 0 1px 0 0 theme('colors.neutral.100');
  656. }
  657. </style>