|
|
@@ -11,13 +11,14 @@ Champs autocomplete dédié à la recherche des access d'une structure
|
|
|
:field="field"
|
|
|
:label="label"
|
|
|
:items="items"
|
|
|
+ item-value="id"
|
|
|
:isLoading="pending"
|
|
|
:multiple="multiple"
|
|
|
:chips="chips"
|
|
|
- no-filter
|
|
|
+ :auto-select-first="false"
|
|
|
prependIcon="fas fa-magnifying-glass"
|
|
|
:return-object="false"
|
|
|
- @update:model-value="$emit('update:model-value', $event)"
|
|
|
+ @update:model-value="onUpdateModelValue"
|
|
|
@update:search="onUpdateSearch"
|
|
|
/>
|
|
|
</main>
|
|
|
@@ -29,6 +30,9 @@ import {computed, ComputedRef, Ref} from "@vue/reactivity";
|
|
|
import {AnyJson, 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'
|
|
|
|
|
|
const props = defineProps({
|
|
|
/**
|
|
|
@@ -89,48 +93,129 @@ const props = defineProps({
|
|
|
chips: {
|
|
|
type: Boolean,
|
|
|
default: false
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * Closes the menu and clear the current search after the selection has been updated
|
|
|
+ */
|
|
|
+ clearSearchAfterUpdate: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
}
|
|
|
})
|
|
|
|
|
|
-const { fetchCollection } = useEntityFetch()
|
|
|
+/**
|
|
|
+ * Element de la liste autocomplete
|
|
|
+ */
|
|
|
+interface AccessListItem {
|
|
|
+ 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 => {
|
|
|
+ return {
|
|
|
+ id: access.id,
|
|
|
+ title: access.person ? `${access.person.givenName} ${access.person.name}` : i18n.t('unknown')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getFromStore = (id: number) => {
|
|
|
+ return em.find(Access, id)
|
|
|
+}
|
|
|
+
|
|
|
+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 = props.filters.value ?? {}
|
|
|
+ let q: AnyJson = {'groups[]': 'access_people_ref'}
|
|
|
+
|
|
|
+ if (!initialized.value) {
|
|
|
+ q['id[in]'] = Array.isArray(props.modelValue) ? props.modelValue.join(',') : props.modelValue
|
|
|
+ return q
|
|
|
+ }
|
|
|
+
|
|
|
if (nameFilter.value !== null) {
|
|
|
q['fullname'] = nameFilter.value
|
|
|
}
|
|
|
- q['groups[]'] = 'access_people_ref'
|
|
|
|
|
|
return q
|
|
|
})
|
|
|
|
|
|
-const { data: collection, pending, refresh } = await fetchCollection(Access, null, query)
|
|
|
-
|
|
|
-const accessToItem = (access: Access): { id: number | string, title: string } => {
|
|
|
- return {
|
|
|
- id: access.id,
|
|
|
- title: access.person ? `${access.person.givenName} ${access.person.name}` : i18n.t('unknown')
|
|
|
+/**
|
|
|
+ * 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
|
|
|
+
|
|
|
+/**
|
|
|
+ * Contenu de la liste autocomplete
|
|
|
+ */
|
|
|
+const items: ComputedRef<Array<AccessListItem>> = computed(() => {
|
|
|
+ let items = props.modelValue.map(getFromStore).map(accessToItem)
|
|
|
+
|
|
|
+ //@ts-ignore
|
|
|
+ const fetchedItems = collection.value.items.map(accessToItem)
|
|
|
+
|
|
|
+ for (let item of fetchedItems) {
|
|
|
+ if (!items.some((existingItem: AccessListItem) => existingItem.id === item.id)) {
|
|
|
+ items.push(item)
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-const items: ComputedRef<Array<{ id: number | string, title: string }>> = computed(() => {
|
|
|
- if (!pending.value && collection.value && collection.value.items) {
|
|
|
- // @ts-ignore
|
|
|
- return collection.value.items.map(accessToItem)
|
|
|
- }
|
|
|
- return []
|
|
|
+ return ArrayUtils.sortObjectsByProp(items, 'title') as Array<AccessListItem>
|
|
|
})
|
|
|
|
|
|
+
|
|
|
+/**
|
|
|
+ * Délai entre le dernier caractère saisi et la requête de vérification de la mise à jour des résultats (en ms)
|
|
|
+ */
|
|
|
+const inputDelay = 600
|
|
|
+
|
|
|
+/**
|
|
|
+ * Version debounced de la fonction refresh
|
|
|
+ * @see https://docs-lodash.com/v4/debounce/
|
|
|
+ */
|
|
|
+const refreshDebounced: _.DebouncedFunc<() => void> = _.debounce(async () => {
|
|
|
+ await refresh();
|
|
|
+}, inputDelay)
|
|
|
+
|
|
|
+// ### Events
|
|
|
const emit = defineEmits(['update:model-value'])
|
|
|
|
|
|
+/**
|
|
|
+ * La recherche textuelle a changé.
|
|
|
+ * @param event
|
|
|
+ */
|
|
|
const onUpdateSearch = (event: string) => {
|
|
|
nameFilter.value = event
|
|
|
- refresh()
|
|
|
+ refreshDebounced()
|
|
|
}
|
|
|
+
|
|
|
+const onUpdateModelValue = (event: Array<number>) => {
|
|
|
+ if (props.clearSearchAfterUpdate) {
|
|
|
+ nameFilter.value = ""
|
|
|
+ }
|
|
|
+ emit('update:model-value', event)
|
|
|
+}
|
|
|
+
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|