ApiResources.vue 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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, WatchStopHandle} 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) {
  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 = computed(() => {
  170. return new Query(
  171. new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
  172. new PageFilter(ref(1), ref(20)),
  173. new InArrayFilter(props.listValue, activeIds),
  174. )
  175. }
  176. )
  177. /**
  178. * On commence par fetcher les models déjà actifs, pour affichage des labels correspondant
  179. */
  180. const {
  181. data: collectionActive,
  182. status: statusActive,
  183. refresh: refreshActive
  184. } = fetchCollection(props.model, null, queryActive.value)
  185. const unwatch: WatchStopHandle = watch(activeIds, ()=>{
  186. refreshActive()
  187. })
  188. /**
  189. * Saisie de l'utilisateur utilisée pour filtrer la recherche
  190. */
  191. const searchFilter: Ref<string | null> = ref(null)
  192. /**
  193. * Query transmise à l'API lors des changements de filtre de recherche
  194. */
  195. const querySearch = new Query(
  196. new OrderBy(props.listLabel, ORDER_BY_DIRECTION.ASC),
  197. new PageFilter(ref(1), ref(20)),
  198. new SearchFilter(props.listLabel, searchFilter, SEARCH_STRATEGY.IPARTIAL),
  199. )
  200. /**
  201. * On fetch les résultats correspondants à la recherche faite par l'utilisateur
  202. */
  203. const {
  204. data: collectionSearch,
  205. status: statusSearch,
  206. refresh: refreshSearch,
  207. } = fetchCollection(props.model, null, querySearch)
  208. //Le pending global dépend des deux recherche (actif, et globale)
  209. const pending = computed(() => statusSearch.value == FETCHING_STATUS.PENDING || statusActive.value == FETCHING_STATUS.PENDING)
  210. /**
  211. * Génère un ListItem à partir des props
  212. * @param searchItem
  213. */
  214. const item = (searchItem: any): ListItem => {
  215. return {
  216. id: searchItem[props.listValue],
  217. title: searchItem[props.listLabel]
  218. ? searchItem[props.listLabel]
  219. : `(${i18n.t('missing_value')})`,
  220. }
  221. }
  222. /**
  223. * Contenu de la liste autocomplete : Les items actifs + les items correspondants à la recherche
  224. */
  225. const items: ComputedRef<Array<ListItem>> = computed(() => {
  226. if (pending.value || !(collectionActive.value && collectionSearch.value)) {
  227. return []
  228. }
  229. const activeItems: ListItem[] = collectionActive.value.items.map(item)
  230. const searchedItems: ListItem[] = collectionSearch.value.items
  231. .map(item)
  232. .filter(
  233. (item) =>
  234. !collectionActive.value!.items.find((other) => other[props.listValue] === item[props.listValue]),
  235. )
  236. return activeItems.concat(searchedItems)
  237. })
  238. /**
  239. * Délai entre le dernier caractère saisi et la requête de vérification de la mise à jour des résultats (en ms)
  240. */
  241. const inputDelay = 400
  242. /**
  243. * Version debounced de la fonction refresh
  244. * @see https://docs-lodash.com/v4/debounce/
  245. */
  246. const refreshDebounced: _.DebouncedFunc<() => void> = _.debounce(async () => {
  247. await refreshSearch()
  248. }, inputDelay)
  249. /**
  250. * La recherche textuelle a changé.
  251. * @param event
  252. */
  253. const onUpdateSearch = (event: string) => {
  254. let search = true
  255. if(searchFilter.value === null){
  256. search = false
  257. }
  258. searchFilter.value = event
  259. if(search){
  260. refreshDebounced()
  261. }
  262. }
  263. // ### Events
  264. const emit = defineEmits(['update:model-value'])
  265. /**
  266. * Quand un item est sélectionné
  267. * @param event
  268. */
  269. const onUpdateModelValue = (event: Array<number>) => {
  270. if (props.clearSearchAfterUpdate) {
  271. searchFilter.value = ''
  272. }
  273. emit('update:model-value', event)
  274. }
  275. // Nettoyer les données lors du démontage du composant
  276. onBeforeUnmount(() => {
  277. // Nettoyer les références du store si nécessaire
  278. if (process.client) {
  279. clearNuxtData('/^' + props.model.entity + '_many_/')
  280. useRepo(props.model).flush()
  281. }
  282. unwatch()
  283. })
  284. </script>
  285. <style scoped lang="scss">
  286. .v-autocomplete {
  287. min-width: 350px;
  288. }
  289. </style>