Browse Source

add PageFilter, update doc, fix Notifications component

Olivier Massot 1 year ago
parent
commit
0880e815e5

+ 10 - 5
components/Layout/Header/Notification.vue

@@ -94,11 +94,14 @@ import type { AnyJson, Pagination } from '~/types/data'
 import { useEntityManager } from '~/composables/data/useEntityManager'
 import UrlUtils from '~/services/utils/urlUtils'
 import NotificationRepository from '~/stores/repositories/NotificationRepository'
+import Query from "~/services/data/Query";
+import PageFilter from "~/services/data/Filters/PageFilter";
 
 const accessProfileStore = useAccessProfileStore()
 
 const isOpen: Ref<boolean> = ref(false)
 const page: Ref<number> = ref(1)
+const itemsPerPage: Ref<number> = ref(5)
 
 const i18n = useI18n()
 const runtimeConfig = useRuntimeConfig()
@@ -109,15 +112,15 @@ const { em } = useEntityManager()
 const { fetchCollection } = useEntityFetch()
 const notificationRepo = useRepo(NotificationRepository)
 
-const query: ComputedRef<AnyJson> = computed(() => {
-  return { page: page.value }
-})
+const query = new Query(
+  new PageFilter(page, itemsPerPage)
+)
 
 const {
   data: collection,
   pending,
   refresh,
-} = await fetchCollection(Notification, null, query)
+} = fetchCollection(Notification, null, query)
 
 /**
  * On récupère les Notifications via le store
@@ -137,7 +140,7 @@ const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
  * Les metadata dépendront de la dernière valeur du GET lancé
  */
 const pagination: ComputedRef<Pagination> = computed(() => {
-  return !pending.value && collection.value !== null
+  return collection.value !== null
     ? collection.value.pagination
     : {}
 })
@@ -173,6 +176,8 @@ const update = async () => {
 
     await refresh()
 
+    console.log(page.value)
+
     // Si des notifications n'avaient pas été marquées comme lues, on le fait immédiatement.
     markNotificationsAsRead()
   }

+ 1 - 1
components/Layout/SubHeader/PersonnalizedList.vue

@@ -63,7 +63,7 @@ const btn: Ref = ref(null)
 
 const { fetchCollection } = useEntityFetch()
 
-const { data: collection, pending } = await fetchCollection(PersonalizedList)
+const { data: collection, pending } = fetchCollection(PersonalizedList)
 
 const i18n = useI18n()
 

+ 1 - 1
components/Ui/Collection.vue

@@ -49,7 +49,7 @@ const { model, parent }: ToRefs = toRefs(props)
 
 const { fetchCollection } = useEntityFetch()
 
-const { data: collection, pending } = await fetchCollection(
+const { data: collection, pending } = fetchCollection(
   model.value,
   parent.value,
 )

+ 11 - 2
doc/entity_manager.md

@@ -118,11 +118,20 @@ calculée (`ComputedRef`), et les données de pagination :
 
 #### L'objet Query et les Filters
 
-...
+Les filtres, les tris et la pagination sont donc empaquetés dans un objet Query : 
 
-#### Réactivité
+    const query = new Query()
 
+On pourra ensuite ajouter des filtres à cette query : 
 
+    query.add(new SearchFilter('name', searchFilter, SEARCH_STRATEGY.IPARTIAL))
+    query.add(new OrderBy('name', ORDER_BY_DIRECTION.ASC))
+    query.add(new PageFilter(page, itemsPerPage))
+
+Cette query traduira ensuite ces conditions en URL query pour l'appel à l'API d'une part, 
+et en query PiniaORM d'autre part.
+
+> NB : les filtres de type 'where' seront toujours appliqués en premier, puis les tris, et enfin la pagination.
 
 ### Reset une entité
 

+ 22 - 3
pages/tests/poc_fetch_collection.vue

@@ -19,7 +19,7 @@ Exemple :
 
     <div class="d-flex flex-row my-8">
       <v-text-field v-model="searchFilter" style="max-width: 200px" />
-      <v-btn class="ma-4" @click="refresh">Refresh</v-btn>
+      <v-btn class="ma-4" @click="onRefreshClick">Refresh</v-btn>
     </div>
 
     <v-row>
@@ -39,6 +39,13 @@ Exemple :
       </v-col>
     </v-row>
 
+    <v-pagination
+      v-model="page"
+      :length="totalPages"
+      :total-visible="7"
+      @update:modelValue="refresh"
+    />
+
     <div class="d-flex flex-row">
       <v-text-field v-model="newVal" class="my-4" style="max-width: 200px" />
       <v-btn class="ma-4" @click="onAddClick">Add organization</v-btn>
@@ -53,20 +60,27 @@ import SearchFilter from '~/services/data/Filters/SearchFilter'
 import {ORDER_BY_DIRECTION, SEARCH_STRATEGY} from '~/types/enum/data'
 import Country from '~/models/Core/Country'
 import OrderBy from "~/services/data/Filters/OrderBy"
+import PageFilter from "~/services/data/Filters/PageFilter";
 
 const { fetchCollection } = useEntityFetch()
 
 const query = new Query()
 
-const searchFilter = ref('fran')
+const searchFilter = ref('')
+
+const newVal = ref('')
 
-const newVal = ref('fran')
+const page = ref(1)
+const itemsPerPage = ref(10)
 
 query.add(new SearchFilter('name', searchFilter, SEARCH_STRATEGY.IPARTIAL))
 query.add(new OrderBy('name', ORDER_BY_DIRECTION.ASC))
+query.add(new PageFilter(page, itemsPerPage))
 
 const { data, pending, refresh } = fetchCollection(Country, null, query)
 
+const totalPages = computed(() => data.value ? data.value?.pagination.last : 1)
+
 console.log(data)
 // console.log(data.value!.items)
 
@@ -85,6 +99,11 @@ const onAddClick = () => {
 
   id += 1
 }
+
+const onRefreshClick = () => {
+  refresh()
+}
+
 </script>
 
 <style scoped lang="scss">

+ 2 - 1
services/data/Filters/OrderBy.ts

@@ -2,6 +2,7 @@ import type { Query as PiniaOrmQuery } from 'pinia-orm'
 import type { ApiFilter } from '~/types/data'
 import ApiResource from '~/models/ApiResource'
 import { ORDER_BY_DIRECTION } from '~/types/enum/data'
+import StringUtils from "~/services/utils/stringUtils";
 
 export default class OrderBy implements ApiFilter {
   field: string
@@ -23,7 +24,7 @@ export default class OrderBy implements ApiFilter {
     query: PiniaOrmQuery<ApiResource>,
   ): PiniaOrmQuery<ApiResource> {
     return query.orderBy(
-      (instance) => instance[this.field].toLowerCase(),
+      (instance) => StringUtils.normalize(instance[this.field]),
       this.mode,
     )
   }

+ 47 - 0
services/data/Filters/PageFilter.ts

@@ -0,0 +1,47 @@
+import type { Query as PiniaOrmQuery } from 'pinia-orm'
+import type { ApiFilter } from '~/types/data'
+import ApiResource from '~/models/ApiResource'
+import { SEARCH_STRATEGY } from '~/types/enum/data'
+
+export default class PageFilter implements ApiFilter {
+  page: Ref<number>
+  itemsPerPage: Ref<number>
+  reactiveFilter: boolean
+
+  /**
+   * @param page
+   * @param itemsPerPage
+   * @param reactiveFilter Est-ce qu'on doit conserver la réactivité du filtre ? Concrètement, dans le cas d'une
+   *                       recherche textuelle, si le filtre est réactif, le résultat de la query Pinia-ORM sera
+   *                       filtré à chaque fois que le filtre est modifié (même sans refresh ou nouvel appel à
+   *                       fetchCollection). Si reactiveFilter est false (comportement par défaut), le résultat
+   *                       de la query ne sera mis à jour qu'en cas de nouvel appel à fetchCollection (ou à refresh()).
+   */
+  constructor(
+    page: Ref<number>,
+    itemsPerPage: Ref<number>,
+    reactiveFilter: boolean = false,
+  ) {
+    this.page = page
+    this.itemsPerPage = itemsPerPage
+    this.reactiveFilter = reactiveFilter
+  }
+
+  public applyToPiniaOrmQuery(
+    query: PiniaOrmQuery<ApiResource>,
+  ): PiniaOrmQuery<ApiResource> {
+    const page = this.reactiveFilter
+      ? this.page
+      : ref(this.page.value)
+
+    const itemsPerPage = this.reactiveFilter
+      ? this.itemsPerPage
+      : ref(this.itemsPerPage.value)
+
+    return query.offset(itemsPerPage.value * (page.value - 1)).limit(itemsPerPage.value)
+  }
+
+  public getApiQueryPart(): string {
+    return `page=${this.page.value}&itemsPerPage=${this.itemsPerPage.value}`
+  }
+}

+ 8 - 0
services/data/Filters/SearchFilter.ts

@@ -40,6 +40,10 @@ export default class SearchFilter implements ApiFilter {
       ? this.filterValue
       : ref(this.filterValue.value)
 
+    if (!filterValue) {
+      return query
+    }
+
     let wordStartRx: RegExp | null = null
     if (this.mode === SEARCH_STRATEGY.WORD_START) {
       wordStartRx = new RegExp(`^${filterValue.value}|\\s${filterValue.value}`)
@@ -67,6 +71,10 @@ export default class SearchFilter implements ApiFilter {
   }
 
   public getApiQueryPart(): string {
+    if (!this.filterValue.value) {
+      return ''
+    }
+
     return `${this.field}[]=${this.filterValue.value}`
   }
 }

+ 23 - 1
services/data/Query.ts

@@ -1,6 +1,9 @@
 import type { Query as PiniaOrmQuery } from 'pinia-orm'
 import type { ApiFilter } from '~/types/data'
 import type ApiResource from '~/models/ApiResource'
+import ObjectUtils from "~/services/utils/objectUtils";
+import PageFilter from "~/services/data/Filters/PageFilter";
+import OrderBy from "~/services/data/Filters/OrderBy";
 
 /**
  * A Query to filter and sort ApiResources.
@@ -39,7 +42,7 @@ export default class Query {
       }
     })
 
-    return queryParts.join('&')
+    return queryParts.filter((p) => !!p).join('&')
   }
 
   /**
@@ -51,9 +54,28 @@ export default class Query {
   public applyToPiniaOrmQuery(
     query: PiniaOrmQuery<ApiResource>,
   ): PiniaOrmQuery<ApiResource> {
+
+    // 'Where' filters shall be applied first, then orderBy filters, and finally pagination
+    const where: ApiFilter[] = []
+    const orderBy: ApiFilter[] = []
+    const pagination: ApiFilter[] = []
+
     this.filters.forEach((filter) => {
+      if (filter instanceof PageFilter) {
+        pagination.push(filter)
+      } else if (filter instanceof OrderBy) {
+        orderBy.push(filter)
+      } else {
+        where.push(filter)
+      }
+    })
+
+    const filters = [...where, ...orderBy, ...pagination]
+
+    filters.forEach((filter) => {
       query = filter.applyToPiniaOrmQuery(query)
     })
+
     return query
   }
 }

+ 12 - 7
services/utils/stringUtils.ts

@@ -7,13 +7,18 @@ export default class StringUtils {
   public static normalize(s: string): string {
     return s
       .toLowerCase()
-      .replace(/[éèẽëê]/g, 'e')
-      .replace(/[ç]/g, 'c')
-      .replace(/[îïĩ]/g, 'i')
-      .replace(/[àã]/g, 'a')
-      .replace(/[öôõ]/g, 'o')
-      .replace(/[ûüũ]/g, 'u')
-      .replace(/[-]/g, ' ')
+      .replace(/[éèẽëêēĕėęě]/g, 'e')
+      .replace(/[çćĉċč]/g, 'c')
+      .replace(/[îïĩìíīĭ]/g, 'i')
+      .replace(/[àãâåáäāăą]/g, 'a')
+      .replace(/[ĝğġģ]/g, 'g')
+      .replace(/[ħĥ]/g, 'h')
+      .replace(/[öôõó]/g, 'o')
+      .replace(/[ûüũùú]/g, 'u')
+      .replace(/[š]/g, 's')
+      .replace(/[ÿý]/g, 'y')
+      .replace(/[ž]/g, 'z')
+      .replace(/-/g, ' ')
       .trim()
   }
 

+ 0 - 1
types/data.d.ts

@@ -51,7 +51,6 @@ interface Collection {
 }
 
 interface ApiFilter {
-  field: string
   applyToPiniaOrmQuery: (query: PiniaOrmQuery<ApiResource>) => PiniaOrmQuery<ApiResource>
   getApiQueryPart: () => string
 }