Table.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <!--
  2. A data table for the parameters page
  3. -->
  4. <template>
  5. <div class="container">
  6. <UiLoadingPanel v-if="pending" />
  7. <v-table v-else>
  8. <thead>
  9. <tr>
  10. <td v-for="col in columns">
  11. {{ col.label }}
  12. </td>
  13. <td>{{ $t('actions') }}</td>
  14. </tr>
  15. </thead>
  16. <tbody v-if="items">
  17. <tr
  18. v-for="(item, i) in items"
  19. :key="i"
  20. >
  21. <td
  22. v-for="col in columns"
  23. class="cycle-editable-cell"
  24. >
  25. {{ item[col.property] }}
  26. </td>
  27. <td class="d-flex flex-row actions-cell">
  28. <v-btn
  29. v-if="actions.includes(TABLE_ACTION.EDIT)"
  30. :flat="true"
  31. icon="fa fa-pen"
  32. class="mr-3"
  33. @click="goToEditPage(item.id as number)"
  34. />
  35. <UiButtonDelete
  36. v-if="actions.includes(TABLE_ACTION.DELETE)"
  37. :entity="item"
  38. :flat="true"
  39. />
  40. </td>
  41. </tr>
  42. </tbody>
  43. <tbody v-else>
  44. <tr class="theme-neutral">
  45. <td>
  46. <i>{{ $t('nothing_to_show') }}</i>
  47. </td>
  48. <td></td>
  49. </tr>
  50. </tbody>
  51. </v-table>
  52. <div class="d-flex justify-end" v-if="actions.includes(TABLE_ACTION.ADD)">
  53. <v-btn
  54. :flat="true"
  55. prepend-icon="fa fa-plus"
  56. class="theme-primary mt-4"
  57. @click="goToCreatePage"
  58. >
  59. {{ $t('add') }}
  60. </v-btn>
  61. </div>
  62. </div>
  63. </template>
  64. <script setup lang="ts">
  65. import {TABLE_ACTION} from '~/types/enum/enums';
  66. import UrlUtils from '~/services/utils/urlUtils';
  67. import type ApiResource from '~/models/ApiResource';
  68. import {useEntityFetch} from '~/composables/data/useEntityFetch';
  69. import type {AssociativeArray} from '~/types/data';
  70. interface ColumnDefinition {
  71. /**
  72. * The entity's property to display in this column
  73. */
  74. property: string,
  75. /**
  76. * Label of the column.
  77. * If not provided, a translation of the property's name will be looked for.
  78. * If none is found, the property's name will be displayed as it is.
  79. */
  80. label?: string
  81. }
  82. const props = defineProps({
  83. /**
  84. * The model whom entities shall be to fetch
  85. */
  86. model: {
  87. type: Object as PropType<typeof ApiResource>,
  88. required: true
  89. },
  90. /**
  91. * The base URL for the edit / create pages
  92. * The resulting url will be constructed this way :
  93. *
  94. * Edition : ({baseUrl}/){apiResource.entity}/{id}
  95. * Creation : ({baseUrl}/){apiResource.entity}/new
  96. */
  97. baseRoute: {
  98. type: String,
  99. required: false,
  100. default: '/parameters'
  101. },
  102. /**
  103. * If provided, define the columns to show.
  104. * Else, all the entity's props are shown.
  105. *
  106. * Ex: [
  107. * { property: 'id', label : 'Identifier'},
  108. * { property: 'name', label : 'Full name'},
  109. * ]
  110. */
  111. columnsDefinitions: {
  112. type: Array as PropType<Array<ColumnDefinition> | null>,
  113. required: false,
  114. default: null
  115. },
  116. /**
  117. * List of the actions available for each record
  118. */
  119. actions: {
  120. type: Array as PropType<Array<TABLE_ACTION>>,
  121. required: false,
  122. default: [TABLE_ACTION.EDIT, TABLE_ACTION.DELETE, TABLE_ACTION.ADD]
  123. },
  124. /**
  125. * If provided, sort the record by the given property
  126. */
  127. sortBy: {
  128. type: String as PropType<string>,
  129. required: false,
  130. default: 'id'
  131. }
  132. })
  133. const i18n = useI18n()
  134. const { fetchCollection } = useEntityFetch()
  135. const {
  136. data: collection,
  137. pending,
  138. } = fetchCollection(props.model)
  139. /**
  140. * Return the properties to display in the table, or all the
  141. * props of the model if not specified.
  142. */
  143. const columns: ComputedRef<Array<ColumnDefinition>> = computed(() => {
  144. let columns: Array<ColumnDefinition>
  145. if (!props.columnsDefinitions) {
  146. const entityProps = Object.getOwnPropertyNames(new props.model())
  147. columns = entityProps.map(prop => {
  148. return {
  149. property: prop,
  150. label: i18n.t(prop)
  151. }
  152. })
  153. } else {
  154. columns = props.columnsDefinitions
  155. columns = columns.map(col => {
  156. return {
  157. property: col.property,
  158. label: col.label ?? i18n.t(col.property)
  159. }
  160. })
  161. }
  162. return columns
  163. })
  164. /**
  165. * Fetch the collection of ApiResources of the given model, then
  166. * map it according to the configuration.
  167. */
  168. const items: ComputedRef<Array<ApiResource> | null> = computed(() => {
  169. if (pending.value || collection.value === null) {
  170. return null
  171. }
  172. let items: Array<ApiResource> = collection.value!.items
  173. if (props.columnsDefinitions !== null) {
  174. // Filter the columns to show
  175. items = items.map(item => {
  176. const newItem: ApiResource = { id: item.id }
  177. for (const col of props.columnsDefinitions) {
  178. newItem[col.property] = item[col.property]
  179. }
  180. return newItem
  181. })
  182. }
  183. if (props.sortBy) {
  184. items = items.sort((a: AssociativeArray, b: AssociativeArray) => {
  185. return (a[props.sortBy as keyof typeof a] > b[props.sortBy as keyof typeof b]) ? 1 : -1
  186. })
  187. }
  188. return items
  189. })
  190. /**
  191. * Redirect to the edition page for the given item
  192. * @param id
  193. */
  194. const goToEditPage = (id: number) => {
  195. console.log(props.baseRoute, props.model.entity, id, UrlUtils.join(props.baseRoute, props.model.entity, id))
  196. navigateTo(UrlUtils.join(props.baseRoute, props.model.entity, id))
  197. }
  198. /**
  199. * Redirect to the creation page for this model
  200. */
  201. const goToCreatePage = () => {
  202. navigateTo(UrlUtils.join(props.baseRoute, props.model.entity, 'new'))
  203. }
  204. </script>
  205. <style scoped lang="scss">
  206. .container {
  207. max-width: 1000px;
  208. }
  209. .v-table {
  210. width: 100%;
  211. thead {
  212. color: rgb(var(--v-theme-neutral-strong));
  213. font-weight: 600;
  214. td {
  215. border-bottom: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
  216. }
  217. td:last-of-type {
  218. padding-left: 30px;
  219. }
  220. }
  221. th, td {
  222. padding: 10px;
  223. text-align: left;
  224. }
  225. td:last-of-type {
  226. width: 125px;
  227. }
  228. }
  229. :deep(.actions-cell .v-icon) {
  230. color: rgb(var(--v-theme-neutral-strong));
  231. font-size: 18px;
  232. }
  233. </style>