ApiResources.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. <!--
  2. Champs autocomplete dédié à la recherche des Accesses d'une structure
  3. @see https://vuetifyjs.com/en/components/autocompletes/#usage
  4. -->
  5. <template>
  6. <main>
  7. <UiInputAutocomplete
  8. :model-value="modelValue"
  9. :field="field"
  10. :label="label"
  11. :items="items"
  12. :item-value="listValue"
  13. :is-loading="pending"
  14. :multiple="multiple"
  15. hide-no-data
  16. :chips="chips"
  17. :closable-chips="true"
  18. :auto-select-first="false"
  19. prepend-inner-icon="fas fa-magnifying-glass"
  20. :return-object="false"
  21. :variant="variant"
  22. :readonly="readonly"
  23. :clearable="true"
  24. @update:model-value="onUpdateModelValue"
  25. @update:search="onUpdateSearch"
  26. />
  27. </main>
  28. </template>
  29. <script setup lang="ts">
  30. import type { PropType, ComputedRef, Ref } from 'vue'
  31. import { computed } from 'vue'
  32. import * as _ from 'lodash-es'
  33. import { useEntityFetch } from '~/composables/data/useEntityFetch'
  34. import Query from '~/services/data/Query'
  35. import OrderBy from '~/services/data/Filters/OrderBy'
  36. import {FETCHING_STATUS, ORDER_BY_DIRECTION, SEARCH_STRATEGY} from '~/types/enum/data'
  37. import PageFilter from '~/services/data/Filters/PageFilter'
  38. import InArrayFilter from '~/services/data/Filters/InArrayFilter'
  39. import SearchFilter from '~/services/data/Filters/SearchFilter'
  40. import type ApiModel from "~/models/ApiModel";
  41. const props = defineProps({
  42. /**
  43. * Valeur actives (tableau si multiple ou ID seule si choix unique)
  44. */
  45. modelValue: {
  46. type: [Array, Number],
  47. required: false,
  48. default: null,
  49. },
  50. /**
  51. * API Resource qui sera fetch
  52. */
  53. model: {
  54. type: Function as PropType<() => typeof ApiModel>,
  55. required: true,
  56. },
  57. /**
  58. * Filtres à transmettre à la source de données
  59. */
  60. query: {
  61. type: Object as PropType<typeof Query>,
  62. required: false,
  63. },
  64. /**
  65. * Nom de la propriété d'une entité lorsque l'input concerne cette propriété
  66. * - Utilisé par la validation
  67. * - Laisser null si le champ ne s'applique pas à une entité
  68. */
  69. field: {
  70. type: String,
  71. required: false,
  72. default: null,
  73. },
  74. /**
  75. * Label du champ
  76. * Si non défini, c'est le nom de propriété qui est utilisé
  77. */
  78. label: {
  79. type: String,
  80. required: false,
  81. default: null,
  82. },
  83. /**
  84. * Définit si le champ est en lecture seule
  85. * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-readonly
  86. */
  87. readonly: {
  88. type: Boolean,
  89. required: false,
  90. },
  91. /**
  92. * Autorise la sélection multiple
  93. * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-multiple
  94. */
  95. multiple: {
  96. type: Boolean,
  97. default: false,
  98. },
  99. /**
  100. * Rends les résultats sous forme de puces
  101. * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-chips
  102. */
  103. chips: {
  104. type: Boolean,
  105. default: false,
  106. },
  107. /**
  108. * Closes the menu and clear the current search after the selection has been updated
  109. */
  110. clearSearchAfterUpdate: {
  111. type: Boolean,
  112. default: false,
  113. },
  114. /**
  115. * @see https://vuetifyjs.com/en/api/v-autocomplete/#props-variant
  116. */
  117. variant: {
  118. type: String as PropType<
  119. | 'filled'
  120. | 'outlined'
  121. | 'plain'
  122. | 'underlined'
  123. | 'solo'
  124. | 'solo-inverted'
  125. | 'solo-filled'
  126. | undefined
  127. >,
  128. required: false,
  129. default: 'outlined',
  130. },
  131. /**
  132. * Nom de la propriété servant à générer les values dans la list
  133. */
  134. listValue: {
  135. type: String,
  136. required: false,
  137. default: null,
  138. },
  139. /**
  140. * Nom de la propriété servant à générer les labels dans la list
  141. */
  142. listLabel: {
  143. type: String,
  144. required: false,
  145. default: null,
  146. },
  147. })
  148. /**
  149. * Element de la liste autocomplete
  150. */
  151. interface ListItem {
  152. id: number | string
  153. title: string
  154. }
  155. const { fetchCollection } = useEntityFetch()
  156. const i18n = useI18n()
  157. const activeIds = computed(() => {
  158. if (Array.isArray(props.modelValue)) {
  159. return props.modelValue
  160. }
  161. if (props.modelValue !== null && typeof props.modelValue === 'object') {
  162. return [props.modelValue]
  163. }
  164. return []
  165. })
  166. /**
  167. * Query transmise à l'API lors de l'initialisation afin de récupérer les items actifs
  168. */
  169. const queryActive = new Query(
  170. new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
  171. new PageFilter(ref(1), ref(20)),
  172. new InArrayFilter(props.listValue, activeIds),
  173. )
  174. /**
  175. * On commence par fetcher les models déjà actifs, pour affichage des labels correspondant
  176. */
  177. const {
  178. data: collectionActive,
  179. status: statusActive
  180. } = fetchCollection(props.model, null, queryActive)
  181. /**
  182. * Saisie de l'utilisateur utilisée pour filtrer la recherche
  183. */
  184. const searchFilter: Ref<string | null> = ref(null)
  185. /**
  186. * Query transmise à l'API lors des changements de filtre de recherche
  187. */
  188. const querySearch = new Query(
  189. new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
  190. new PageFilter(ref(1), ref(20)),
  191. new SearchFilter(props.listLabel, searchFilter, SEARCH_STRATEGY.IPARTIAL),
  192. )
  193. /**
  194. * On fetch les résultats correspondants à la recherche faite par l'utilisateur
  195. */
  196. const {
  197. data: collectionSearch,
  198. status: statusSearch,
  199. refresh: refreshSearch,
  200. } = fetchCollection(props.model, null, querySearch)
  201. //Le pending global dépend des deux recherche (actif, et globale)
  202. const pending = computed(() => statusSearch.value == FETCHING_STATUS.PENDING || statusActive.value == FETCHING_STATUS.PENDING)
  203. /**
  204. * Génère un ListItem à partir des props
  205. * @param searchItem
  206. */
  207. const item = (searchItem: any): ListItem => {
  208. return {
  209. id: searchItem[props.listValue],
  210. title: searchItem[props.listLabel]
  211. ? searchItem[props.listLabel]
  212. : `(${i18n.t('missing_value')})`,
  213. }
  214. }
  215. /**
  216. * Contenu de la liste autocomplete : Les items actifs + les items correspondants à la recherche
  217. */
  218. const items: ComputedRef<Array<ListItem>> = computed(() => {
  219. if (pending.value || !(collectionActive.value && collectionSearch.value)) {
  220. return []
  221. }
  222. const activeItems: ListItem[] = collectionActive.value.items.map(item)
  223. const searchedItems: ListItem[] = collectionSearch.value.items
  224. .map(item)
  225. .filter(
  226. (item) =>
  227. !collectionActive.value!.items.find((other) => other[props.listValue] === item[props.listValue]),
  228. )
  229. return activeItems.concat(searchedItems)
  230. })
  231. /**
  232. * Délai entre le dernier caractère saisi et la requête de vérification de la mise à jour des résultats (en ms)
  233. */
  234. const inputDelay = 400
  235. /**
  236. * Version debounced de la fonction refresh
  237. * @see https://docs-lodash.com/v4/debounce/
  238. */
  239. const refreshDebounced: _.DebouncedFunc<() => void> = _.debounce(async () => {
  240. await refreshSearch()
  241. }, inputDelay)
  242. // ### Events
  243. const emit = defineEmits(['update:model-value'])
  244. /**
  245. * La recherche textuelle a changé.
  246. * @param event
  247. */
  248. const onUpdateSearch = (event: string) => {
  249. let search = true
  250. if(searchFilter.value === null){
  251. search = false
  252. }
  253. searchFilter.value = event
  254. if(search){
  255. refreshDebounced()
  256. }
  257. }
  258. /**
  259. * Quand un item est sélectionné
  260. * @param event
  261. */
  262. const onUpdateModelValue = (event: Array<number>) => {
  263. if (props.clearSearchAfterUpdate) {
  264. searchFilter.value = ''
  265. }
  266. emit('update:model-value', event)
  267. }
  268. // Nettoyer les données lors du démontage du composant
  269. onBeforeUnmount(() => {
  270. // Nettoyer les références du store si nécessaire
  271. if (process.client) {
  272. clearNuxtData('/^' + props.model.entity + '_many_/')
  273. useRepo(props.model).flush()
  274. }
  275. })
  276. </script>
  277. <style scoped lang="scss">
  278. .v-autocomplete {
  279. min-width: 350px;
  280. }
  281. </style>