Explorar o código

fix UiAutocompleteAccesses

Olivier Massot hai 10 meses
pai
achega
cfc135037b

+ 5 - 1
components/Ui/Form.vue

@@ -172,7 +172,6 @@ const i18n = useI18n()
 const router = useRouter()
 const { em } = useEntityManager()
 const { refreshProfile } = useRefreshProfile()
-const route = useRoute()
 
 // Le formulaire est-il valide
 const isValid: Ref<boolean> = ref(true)
@@ -210,6 +209,9 @@ const closeConfirmationDialog = () => {
   formStore.setShowConfirmToLeave(false)
 }
 
+const emit = defineEmits(['update:entity'])
+
+
 // ### Actions du formulaire
 /**
  * Soumet le formulaire
@@ -235,6 +237,8 @@ const submit = async (next: string | null = null) => {
     // TODO: est-ce qu'il faut re-fetch l'entité après le persist?
     const updatedEntity = await em.persist(props.entity)
 
+    emit('update:entity', updatedEntity)
+
     if (props.refreshProfile) {
       await refreshProfile()
     }

+ 57 - 65
components/Ui/Input/Autocomplete/Accesses.vue

@@ -1,5 +1,5 @@
 <!--
-Champs autocomplete dédié à la recherche des access d'une structure
+Champs autocomplete dédié à la recherche des Accesses d'une structure
 
 @see https://vuetifyjs.com/en/components/autocompletes/#usage
 -->
@@ -28,18 +28,23 @@ Champs autocomplete dédié à la recherche des access d'une structure
 
 <script setup lang="ts">
 import type { PropType } from '@vue/runtime-core'
-import { computed } from '@vue/reactivity'
 import type { ComputedRef, Ref } from '@vue/reactivity'
-import type { AnyJson, AssociativeArray } from '~/types/data'
+import { computed } from '@vue/reactivity'
+import type { AssociativeArray } from '~/types/data'
 import { useEntityFetch } from '~/composables/data/useEntityFetch'
 import Access from '~/models/Access/Access'
-import { useEntityManager } from '~/composables/data/useEntityManager'
-import ArrayUtils from '~/services/utils/arrayUtils'
 import * as _ from 'lodash-es'
+import Query from '~/services/data/Query'
+import OrderBy from '~/services/data/Filters/OrderBy'
+import {ORDER_BY_DIRECTION, SEARCH_STRATEGY} from '~/types/enum/data'
+import PageFilter from '~/services/data/Filters/PageFilter';
+import InArrayFilter from '~/services/data/Filters/InArrayFilter';
+import SearchFilter from '~/services/data/Filters/SearchFilter';
+import UserSearchItem from '~/models/Custom/Search/UserSearchItem';
 
 const props = defineProps({
   /**
-   * v-model
+   * v-model, ici les ids des Access sélectionnés
    */
   modelValue: {
     type: [Object, Array],
@@ -48,6 +53,7 @@ const props = defineProps({
   },
   /**
    * Filtres à transmettre à la source de données
+   * TODO: voir si à adapter maintenant que les filtres sont des objets Query
    */
   filters: {
     type: Object as PropType<Ref<AssociativeArray>>,
@@ -126,101 +132,87 @@ const props = defineProps({
 /**
  * Element de la liste autocomplete
  */
-interface AccessListItem {
+interface UserListItem {
   id: number | string
   title: string
 }
 
 const { fetchCollection } = useEntityFetch()
-const { em } = useEntityManager()
 const i18n = useI18n()
 
 /**
  * Génère un AccessListItem à partir d'un Access
  * @param access
  */
-const accessToItem = (access: Access): AccessListItem => {
+const accessToItem = (userSearchItem: UserSearchItem): UserListItem => {
   return {
-    id: access.id,
-    title: access.person
-      ? `${access.person.name} ${access.person.givenName}`
-      : i18n.t('unknown'),
+    id: userSearchItem.id,
+    title: userSearchItem.fullName ? userSearchItem.fullName : `(${i18n.t('missing_name')})`,
   }
 }
 
-const initialized: Ref<boolean> = ref(false)
-
 /**
  * Saisie de l'utilisateur utilisée pour filtrer la recherche
  */
 const nameFilter: Ref<string | null> = ref(null)
 
-/**
- * Query transmise à l'API lors des changements de filtre de recherche
- */
-const query: ComputedRef<AnyJson> = computed(() => {
-  let q: AnyJson = { 'groups[]': 'access_people_ref', 'order[name]': 'asc' }
-
-  if (!initialized.value && props.modelValue) {
-    if (Array.isArray(props.modelValue) && props.modelValue.length > 0) {
-      q['id[in]'] = props.modelValue.join(',')
-    } else {
-      q['id[in]'] = props.modelValue
-    }
-    return q
+const activeIds = computed(() => {
+  if (Array.isArray(props.modelValue)) {
+    return props.modelValue
   }
-
-  if (nameFilter.value) {
-    q['fullname'] = nameFilter.value
+  if (props.modelValue !== null && typeof props.modelValue === 'object') {
+    return [props.modelValue.id]
   }
-
-  return q
+  return []
 })
 
+const queryActive = new Query(
+  new OrderBy('name', ORDER_BY_DIRECTION.ASC),
+  new PageFilter(ref(1), ref(10)),
+  new InArrayFilter('id', activeIds)
+)
+
+const {
+  data: collectionActive,
+  pending: pendingActive,
+} = fetchCollection(UserSearchItem, null, queryActive)
+
+
+/**
+ * Query transmise à l'API lors des changements de filtre de recherche
+ */
+const querySearch = new Query(
+  new OrderBy('name', ORDER_BY_DIRECTION.ASC),
+  new PageFilter(ref(1), ref(100)),
+  new SearchFilter('fullName', nameFilter, SEARCH_STRATEGY.IPARTIAL)
+)
+
 /**
  * On commence par fetcher les accesses déjà actifs, pour affichage des noms
  */
 const {
-  data: collection,
-  pending,
-  refresh,
-} = await fetchCollection(Access, null, query)
-initialized.value = true
+  data: collectionSearch,
+  pending: pendingSearch,
+  refresh: refreshSearch,
+} = fetchCollection(UserSearchItem, null, querySearch)
+
+const pending = computed(() => pendingSearch.value || pendingActive.value)
 
-// On a déjà récupéré les access actifs, on relance une requête pour récupérer la première page
-// des accesses suivants
-refresh()
 
 /**
  * Contenu de la liste autocomplete
  */
-const items: ComputedRef<Array<AccessListItem>> = computed(() => {
-  if (pending.value || !collection.value) {
+const items: ComputedRef<Array<UserListItem>> = computed(() => {
+  if (pending.value || !(collectionActive.value && collectionSearch.value)) {
     return []
   }
 
-  if (!collection.value) {
-    return []
-  }
-
-  //@ts-ignore
-  const fetchedItems = collection.value.items.map(accessToItem)
-
-  // move the active items to the top and sort by name
-  fetchedItems.sort((a, b) => {
-    if (props.modelValue.includes(a.id) && !props.modelValue.includes(b.id)) {
-      return -1
-    } else if (
-      !props.modelValue.includes(a.id) &&
-      props.modelValue.includes(b.id)
-    ) {
-      return 1
-    } else {
-      return a.title.localeCompare(b.title)
-    }
-  })
+  const activeItems: UserListItem[] = collectionActive.value.items.map(accessToItem)
+  const searchedItems: UserListItem[] = collectionSearch.value.items
+    .map(accessToItem)
+    .filter(item => !collectionActive.value.items.find(other => other.id === item.id))
 
-  return fetchedItems
+  return activeItems.concat(searchedItems)
 })
 
 /**
@@ -233,7 +225,7 @@ const inputDelay = 600
  * @see https://docs-lodash.com/v4/debounce/
  */
 const refreshDebounced: _.DebouncedFunc<() => void> = _.debounce(async () => {
-  await refresh()
+  await refreshSearch()
 }, inputDelay)
 
 // ### Events

+ 2 - 1
i18n/lang/fr.json

@@ -704,5 +704,6 @@
   "An error occured": "Une erreur s'est produite.",
   "teachers": "Professeurs",
   "pupils-members": "Élèves / Adhérents / Membres",
-  "id": "Id"
+  "id": "Id",
+  "missing_name": "Nom manquant"
 }

+ 30 - 0
models/Custom/Search/UserSearchItem.ts

@@ -0,0 +1,30 @@
+import { Num, Uid, Attr, Str } from 'pinia-orm/dist/decorators'
+import type { Historical } from '~/types/interfaces'
+import Person from '~/models/Person/Person'
+import ApiModel from '~/models/ApiModel'
+import { IriEncoded } from '~/models/decorators'
+import Organization from '~/models/Organization/Organization'
+
+/**
+ * AP2i Model : UserSearchItem
+ *
+ * @see https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/src/Entity/Custom/Search/UserSearchItem.php
+ */
+export default class UserSearchItem extends ApiModel {
+  static override entity = 'search/users'
+
+  @Uid()
+  declare id: number | string
+
+  @Str('')
+  declare username: string
+
+  @Str('')
+  declare name: string
+
+  @Str('')
+  declare givenName: string
+
+  @Str('')
+  declare fullName: string
+}

+ 2 - 2
pages/parameters/website.vue

@@ -5,7 +5,7 @@
       v-else-if="parameters !== null"
       :model="Parameters"
       :entity="parameters"
-      action-position="bottom"
+      @update:entity="refresh"
     >
       <v-row>
         <v-col cols="12">
@@ -158,7 +158,7 @@ if (organizationProfile.parametersId === null) {
   throw new Error('Missing organization parameters id')
 }
 
-const { data: parameters, pending } = fetch(
+const { data: parameters, pending, refresh } = fetch(
   Parameters,
   organizationProfile.parametersId,
 ) as AsyncData<ApiResource | null, Error | null>

+ 61 - 0
services/data/Filters/InArrayFilter.ts

@@ -0,0 +1,61 @@
+import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import type { Ref } from 'vue'
+import type { ApiFilter } from '~/types/data'
+import ApiResource from '~/models/ApiResource'
+import AbstractFilter from '~/services/data/Filters/AbstractFilter'
+import RefUtils from '~/services/utils/refUtils'
+
+export default class InArrayFilter extends AbstractFilter implements ApiFilter {
+  field: string
+  filterValue: Array<string | number | null> | Ref<Array<string | number | null>> | null
+
+  /**
+   * @param field
+   * @param value
+   * @param reactiveFilter
+   */
+  constructor(
+    field: string,
+    value: Array<string | number | null> | Ref<Array<string | number | null>> | null,
+    reactiveFilter: boolean = false,
+  ) {
+    super(reactiveFilter)
+    this.field = field
+    this.filterValue = value
+  }
+
+  public applyToPiniaOrmQuery(
+    query: PiniaOrmQuery<ApiResource>,
+  ): PiniaOrmQuery<ApiResource> {
+    const filterValue = RefUtils.castToRef(
+      this.filterValue,
+      this.reactiveFilter,
+    )
+
+    if (filterValue.value === null) {
+      return query
+    }
+
+    return query.whereIn(this.field, filterValue.value)
+  }
+
+  public getApiQueryPart(): string {
+    const filterValue = RefUtils.castToRef(
+      this.filterValue,
+      this.reactiveFilter,
+    )
+    if (filterValue.value === null) {
+      return ''
+    }
+
+    if (!Array.isArray(filterValue.value)) {
+      filterValue.value = [filterValue.value]
+    }
+
+    if (!filterValue.value.length > 0) {
+      return ''
+    }
+
+    return `${this.field}[in]=${filterValue.value.join(',')}`
+  }
+}

+ 8 - 0
services/data/Query.ts

@@ -26,6 +26,14 @@ export default class Query {
     return this
   }
 
+  /**
+   * Clear the query filters
+   */
+  public clear(): this {
+    this.filters = []
+    return this
+  }
+
   /**
    * Returns the URL's query in the Api Platform format.
    *

+ 2 - 1
services/data/entityManager.ts

@@ -318,7 +318,8 @@ class EntityManager {
     const body = JSON.stringify(data)
     const response = await this.apiRequestService.put(url, body)
 
-    const hydraResponse = await HydraNormalizer.denormalize(response, model)
+    const hydraResponse = HydraNormalizer.denormalize(response, model)
+
     return this.newInstance(model, hydraResponse.data)
   }
 

+ 2 - 2
services/utils/refUtils.ts

@@ -3,8 +3,8 @@ import { ref, isRef } from 'vue'
 
 export default class RefUtils {
   /**
-   * Convertit la valeur du filtre en référence. S'il s'agit déjà d'une ref,
-   * selon que `maintainReactivity` est vrai ou faux, on conserve la référence existante
+   * Convertit la valeur passée en référence.
+   * S'il s'agit déjà d'une ref, selon que `maintainReactivity` est vrai ou faux, on conserve la référence existante
    * ou bien on la recréé pour briser la réactivité.
    *
    * @param value