浏览代码

Merge branch 'feature/BTTF-20' into develop

# Conflicts:
#	lang/layout/fr-FR.js
#	types/interfaces.d.ts
Vincent GUFFON 4 年之前
父节点
当前提交
82bb7d3145
共有 40 个文件被更改,包括 746 次插入207 次删除
  1. 6 0
      assets/css/global.scss
  2. 二进制
      assets/images/byDefault/default_pic.jpeg
  3. 5 8
      components/Layout/Header/Menu.vue
  4. 224 32
      components/Layout/Header/Notification.vue
  5. 1 1
      components/Layout/SubHeader/PersonnalizedList.vue
  6. 1 0
      config/nuxtConfig/vuetify.js
  7. 21 15
      lang/layout/fr-FR.js
  8. 2 2
      models/Access/MyProfile.ts
  9. 2 2
      models/Access/PersonalizedList.ts
  10. 2 2
      models/Core/AddressPostal.ts
  11. 2 2
      models/Core/BankAccount.ts
  12. 2 2
      models/Core/ContactPoint.ts
  13. 2 2
      models/Core/Country.ts
  14. 24 0
      models/Core/Notification.ts
  15. 17 0
      models/Core/NotificationMessage.ts
  16. 17 0
      models/Core/NotificationUsers.ts
  17. 2 2
      models/Organization/Organization.ts
  18. 2 2
      models/Organization/OrganizationAddressPostal.ts
  19. 2 2
      models/Organization/OrganizationLicence.ts
  20. 2 2
      models/Organization/OrganizationNetwork.ts
  21. 22 10
      services/connection/connection.ts
  22. 23 20
      services/connection/urlBuilder.ts
  23. 57 0
      services/connection/urlOptionsBuilder.ts
  24. 1 1
      services/data/dataProvider.ts
  25. 1 3
      services/data/processor/baseProcessor.ts
  26. 3 3
      services/data/processor/enumProcessor.ts
  27. 5 3
      services/data/processor/imageProcessor.ts
  28. 9 6
      services/data/processor/modelProcessor.ts
  29. 80 44
      services/serializer/denormalizer/hydra.ts
  30. 23 3
      services/serializer/normalizer/model.ts
  31. 20 3
      services/store/repository.ts
  32. 20 0
      services/utils/typesTesting.ts
  33. 1 1
      store/index.js
  34. 36 0
      tests/unit/services/connection/urlOptionsBuilder.spec.ts
  35. 21 20
      tests/unit/services/serializer/denormalizer/hydra.spec.ts
  36. 0 8
      tests/unit/services/utils/apiError.spec.ts
  37. 47 0
      tests/unit/services/utils/typesTesting.spec.ts
  38. 11 0
      types/enums.ts
  39. 25 5
      types/interfaces.d.ts
  40. 5 1
      types/types.d.ts

+ 6 - 0
assets/css/global.scss

@@ -22,4 +22,10 @@ header .v-toolbar__content{
   color: var(--v-ot_green-base, white)
 }
 
+.header_menu{
+  max-height: 300px;
+  min-width: 300px;
+  overflow-y: scroll
+}
+
 

二进制
assets/images/byDefault/default_pic.jpeg


+ 5 - 8
components/Layout/Header/Menu.vue

@@ -9,7 +9,6 @@ header principal (configuration, paramètres du compte...)
       <v-tooltip bottom>
         <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
           <v-btn
-
             icon
             v-bind="[attrs, attrs_tooltips]"
             color=""
@@ -31,16 +30,15 @@ header principal (configuration, paramètres du compte...)
       </v-tooltip>
     </template>
     <v-card scrollable>
-      <v-card-title class="ot_super_light_grey text-body-2 font-weight-bold">
+      <v-card-title class="ot_header_menu text-body-2 font-weight-bold">
         {{$t(menu.title)}}
       </v-card-title>
-      <v-card-text style="max-height: 300px; overflow-y: scroll" class="ma-0 pa-0">
+      <v-card-text class="ma-0 pa-0 header_menu">
         <v-list dense :subheader="true">
           <template v-for="(item, index) in menu.children">
             <v-list-item
-
               :id="item.title"
-              :key="item.title"
+              :key="index"
               :href="item.isExternalLink ? item.to : undefined"
               :to="!item.isExternalLink ? item.to : undefined"
               router
@@ -54,14 +52,13 @@ header principal (configuration, paramètres du compte...)
       <v-card-actions class="ma-0 pa-0">
         <template v-for="(item, index) in menu.actions">
           <v-list-item
-            class="text-body-2"
             :id="item.title"
-            :key="item.title"
+            :key="index"
             :href="item.isExternalLink ? item.to : undefined"
             :to="!item.isExternalLink ? item.to : undefined"
             router
           >
-            <v-list-item-title v-text="$t(item.title)"/>
+            <v-list-item-title class="text-body-2 ot_white--text" v-text="$t(item.title)"/>
           </v-list-item>
         </template>
       </v-card-actions>

+ 224 - 32
components/Layout/Header/Notification.vue

@@ -1,59 +1,251 @@
-<!--
-Notification
--->
-
 <template>
-  <v-menu offset-y>
-    <template v-slot:activator="{ on, attrs }">
+  <v-menu offset-y v-model="isOpen">
+    <template v-slot:activator="{ on: { click }, attrs }">
       <v-tooltip bottom>
-        <template v-slot:activator="{ on, attrs }">
+        <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
           <v-btn
             icon
-            v-bind="attrs"
-            v-on="on"
+            v-bind="[attrs, attrs_tooltips]"
+            color=""
+            v-on="on_tooltips"
+            @click="click"
           >
-            <v-icon class="ot_white--text" small>
-              fa-bell
-            </v-icon>
+            <v-badge
+              color="orange"
+              offset-y="10"
+              :value="unreadNotification.length > 0"
+              :content="unreadNotification.length"
+            >
+              <v-icon class="ot_white--text" small>
+                fa-bell
+              </v-icon>
+            </v-badge>
           </v-btn>
         </template>
         <span>{{ $t('notification') }}</span>
       </v-tooltip>
     </template>
-    <v-list dense>
-      <template v-for="(item, index) in properties.menu">
-        <v-list-item :key="item.title">
-          <v-list-item-title v-text="$t(item.title)" />
-        </v-list-item>
-        <v-divider
-          v-if="index < properties.menu.length - 1"
-          :key="index"
-        />
-      </template>
-    </v-list>
+    <v-card scrollable max-width="400">
+      <v-card-title class="ot_header_menu text-body-2 font-weight-bold">
+        {{ $t('notification') }}
+      </v-card-title>
+      <v-card-text class="ma-0 pa-0 header_menu">
+        <v-list dense :subheader="true">
+          <template v-for="(notification, index) in notifications">
+            <v-list-item :key="index" :class="`${notification.notificationUsers.length === 0 ? 'unread' : ''}`">
+              <v-list-item-content>
+                <v-list-item-title class="list_item mt-2 mb-2" v-text="getMessage(notification)"/>
+              </v-list-item-content>
+              <v-list-item-icon v-if="notification.link" class="pt-4">
+                <v-icon @click="download(notification.link)">mdi-download</v-icon>
+              </v-list-item-icon>
+            </v-list-item>
+            <v-divider></v-divider>
+          </template>
+        </v-list>
+        <v-card v-intersect="update"></v-card>
+        <v-row
+          v-if="loading"
+          class="fill-height mt-3 mb-3"
+          align="center"
+          justify="center"
+        >
+          <v-progress-circular
+            indeterminate
+            color="grey lighten-1"
+          ></v-progress-circular>
+        </v-row>
+      </v-card-text>
+      <v-card-actions class="ma-0 pa-0">
+        <template>
+          <v-list-item
+            id="all_notifications"
+            :key="$t('all_notification')"
+            :href="notificationUrl"
+            router
+          >
+            <v-list-item-title class="text-body-2 ot_white--text" v-text="$t('all_notification')"/>
+          </v-list-item>
+        </template>
+      </v-card-actions>
+    </v-card>
   </v-menu>
 </template>
 
 <script lang="ts">
-import { defineComponent, reactive, Ref, UnwrapRef } from '@nuxtjs/composition-api'
-import { $useMenu } from '@/use/layout/menu'
-import { AnyJson } from '~/types/interfaces'
+import {computed, ComputedRef, defineComponent, Ref, ref, useContext, useFetch, useStore, watch} from '@nuxtjs/composition-api'
+import {NOTIFICATION_TYPE, QUERY_TYPE} from "~/types/enums";
+import {Notification} from "~/models/Core/Notification";
+import {repositoryHelper} from "~/services/store/repository";
+import {AnyStore, ApiResponse, HydraMetadata} from "~/types/interfaces";
+import {queryHelper} from "~/services/store/query";
+import {NotificationUsers} from "~/models/Core/NotificationUsers";
+import {State} from "@vuex-orm/core";
 
 export default defineComponent({
-  setup () {
-    const menu: Ref<any> = $useMenu.setupContext().useConfigurationMenuConstruct()
+  setup: function () {
+    const {$dataProvider, $dataPersister, $config, app: { i18n }} = useContext()
+    const store:AnyStore = useStore<State>()
+    const profileAccess = store.state.profile.access
+    const loading: Ref<Boolean> = ref(true)
+    const isOpen: Ref<Boolean> = ref(false)
+    const page: Ref<number> = ref(1)
+    const data: Ref<ApiResponse> = ref({} as ApiResponse)
+
+    /**
+     * On récupère les notifications via l'API qui seront stockées dans le store
+     */
+    const {fetch, fetchState} = useFetch(async () => {
+      data.value = await $dataProvider.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: Notification,
+        listArgs: {
+          itemsPerPage: 10,
+          page: page.value
+        }
+      })
+      loading.value = false
+    })
+
+    /**
+     * On récupère les Notifications via le store
+     */
+    const notifications: ComputedRef = computed(() => {
+      const query = repositoryHelper.getRepository(Notification).with('message').orderBy('id', 'desc')
+      return queryHelper.getCollection(query)
+    })
+
+    /**
+     * on calcul le nombre de notification non lues
+     */
+    const unreadNotification: ComputedRef<Array<Notification>> = computed(() => {
+      return notifications.value.filter((notification: Notification) => {
+        return notification.notificationUsers.length === 0
+      })
+    })
+
+    /**
+     * Les metadata dépendront de la dernière valeur du GET lancé
+     */
+    const metadata: ComputedRef<HydraMetadata> = computed(() => {
+      return data.value.metadata
+    })
+
+    /**
+     * Lorsque l'utilisateur scroll on regarde la nextPage a charger et on le fait que si le pending du fetch est false
+     * (si on a fini de télécharger les éléments précédents)
+     */
+    const update = async () => {
+      if (!fetchState.pending && metadata.value?.nextPage && metadata.value.nextPage > 0) {
+        loading.value = true
+        page.value = metadata.value.nextPage
+        await fetch()
+        //Si des notifications n'avaient pas été marquées comme lues, on le fait immédiatement.
+        markNotificationsAsRead()
+      }
+    }
+
+    /**
+     * On construit le message qui va devoir s'afficher pour une notification
+     * @param notification
+     */
+    const getMessage = (notification:Notification) => {
+      switch (notification.type){
+        case NOTIFICATION_TYPE.FILE :
+         return `${i18n.t('your_file')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be_downloaded')}`
+          break;
+
+         case NOTIFICATION_TYPE.MESSAGE:
+           if(notification.message?.action)
+             return `${i18n.t('your_message')} ${notification.message?.fileName} ${i18n.t('is_ready_to_be')} ${notification.message.action}`
 
-    const properties: UnwrapRef<AnyJson> = reactive({
-      menu
+           return `${i18n.t('your_message')} ${notification.message?.about} ${i18n.t('has_been_sent')} `
+           break;
+
+        default:
+          return i18n.t(notification.name)
+      }
+    }
+
+    /**
+     * Dès l'ouverture du menu, on indique que les notifications non lues, le sont.
+     */
+    watch(isOpen, (newValue, oldValue) => {
+      if(newValue){
+        markNotificationsAsRead()
+      }
     })
 
+    /**
+     * Marque les notification non lues comme lues
+     */
+    const markNotificationsAsRead = () => {
+      unreadNotification.value.map((notification:Notification)=>{
+        notification.notificationUsers = ['read']
+        repositoryHelper.persist(Notification, notification)
+        createNewNotificationUsers(notification)
+      })
+    }
+
+    /**
+     * Créer une nouvelle notification users coté back.
+     * @param notification
+     */
+    const createNewNotificationUsers = (notification: Notification) =>{
+      const newNotificationUsers = repositoryHelper.persist(NotificationUsers, new NotificationUsers(
+        {
+          access:`/api/accesses/${profileAccess.id}`,
+          notification:`/api/notifications/${notification.id}`,
+          isRead: true
+        }
+      )) as NotificationUsers
+
+      $dataPersister.invoke({
+        type: QUERY_TYPE.MODEL,
+        model: NotificationUsers,
+        idTemp: newNotificationUsers.id,
+        showProgress: false
+      })
+    }
+
+    /**
+     * Download le lien
+     * @param link
+     */
+    const download = (link: string) => {
+      const url_parts: Array<string> = link.split('/api');
+      if(profileAccess.originalAccess)
+        url_parts[0] = `api/${profileAccess.originalAccess}/${profileAccess.id}`
+      else
+        url_parts[0] = `api/${profileAccess.id}`
+
+      window.open(`${$config.baseURL_Legacy}/${url_parts.join('')}`);
+    }
+
     return {
-      properties
+      data,
+      getMessage,
+      notificationUrl: `${$config.baseURL_adminLegacy}/notifications/list/`,
+      loading,
+      notifications,
+      update,
+      unreadNotification,
+      isOpen,
+      download
     }
   }
 })
 </script>
 
 <style scoped>
-
+  #all_notifications{
+    background: var(--v-ot_green-base, white);
+    color: white;
+  }
+  .list_item{
+    white-space: normal;
+  }
+  .unread{
+    background: #ecf0f5;
+  }
 </style>

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

@@ -62,7 +62,7 @@ export default defineComponent({
     })
 
     const items:ComputedRef<Array<AnyJson>> = computed(() => {
-      const lists = repositoryHelper.findCollectionFromModel(PersonalizedList) as Collection<PersonalizedList>
+      const lists = repositoryHelper.findCollectionFromModel(PersonalizedList, {'id':'desc'}) as Collection<PersonalizedList>
 
       let listsGroupByKeyMenu:Array<AnyJson> = groupListByKey(lists)
 

+ 1 - 0
config/nuxtConfig/vuetify.js

@@ -29,6 +29,7 @@ export default {
           ot_light_green: '#a9e0d6',
           ot_dark_grey: '#2c3a48',
           ot_grey: '#777777',
+          ot_header_menu: '#ECE7E5',
           ot_light_grey: '#f5f5f5',
           ot_super_light_grey: '#ecf0f5',
           ot_danger: '#f56954',

+ 21 - 15
lang/layout/fr-FR.js

@@ -122,20 +122,26 @@ export default (context, locale) => {
     paying_structure: 'Établissement payeur',
     no_bill_to_display: 'Aucune facture à afficher',
     my_account: 'Mon compte',
-    my_schedule_page: 'Mon planning',
-    attendance_bookings_menu: 'Gestion des absences & fiches de présence',
-    my_attendance: 'Mes absences',
-    my_invitation: 'Mes invitations',
-    my_students: 'Mes élèves',
-    my_students_education_students: 'Suivi pédagogique',
-    my_education_students: 'Mes évaluations',
-    send_an_email: 'Envoyer un email',
-    my_documents: 'Mes documents',
-    my_profile: 'Mon profil',
-    adherent_list: 'Liste des adhérents avec leurs coordonnées',
-    my_subscription: 'Mon abonnement',
-    my_bills: 'Mes factures',
-    print_my_licence: 'Imprimer ma licence CMF',
-    logout: 'Se déconnecter'
+    my_schedule_page: "Mon planning",
+    attendance_bookings_menu: "Gestion des absences & fiches de présence",
+    my_attendance: "Mes absences",
+    my_invitation: "Mes invitations",
+    my_students: "Mes élèves",
+    my_students_education_students: "Suivi pédagogique",
+    my_education_students: "Mes évaluations",
+    send_an_email: "Envoyer un email",
+    my_documents: "Mes documents",
+    my_profile: "Mon profil",
+    adherent_list: "Liste des adhérents avec leurs coordonnées",
+    my_subscription: "Mon abonnement",
+    my_bills: "Mes factures",
+    print_my_licence: "Imprimer ma licence CMF",
+    logout: "Se déconnecter",
+    all_notification: "Toutes les notifications",
+    your_file: "Votre fichier",
+    is_ready_to_be_downloaded: "est près à être téléchargé",
+    your_message: "Votre message",
+    has_been_sent: "a été envoyé",
+    ready_to_be: "est prêt à être",
   })
 }

+ 2 - 2
models/Access/MyProfile.ts

@@ -1,10 +1,10 @@
-import { Attr, Num, Model } from '@vuex-orm/core'
+import {Attr, Num, Model, Uid} from '@vuex-orm/core'
 import { Historical } from '~/types/interfaces'
 
 export class MyProfile extends Model {
   static entity = 'accesses'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Num(0, { nullable: true })

+ 2 - 2
models/Access/PersonalizedList.ts

@@ -1,9 +1,9 @@
-import { Attr, Model, Str } from '@vuex-orm/core'
+import {Model, Str, Uid} from '@vuex-orm/core'
 
 export class PersonalizedList extends Model {
   static entity = 'personalized_lists'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Str('')

+ 2 - 2
models/Core/AddressPostal.ts

@@ -1,10 +1,10 @@
-import { Attr, Str, HasOne, Num, Model } from '@vuex-orm/core'
+import {Str, HasOne, Num, Model, Uid} from '@vuex-orm/core'
 import { Country } from '~/models/Core/Country'
 
 export class AddressPostal extends Model {
   static entity = 'address_postals'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @HasOne(() => Country, 'id')

+ 2 - 2
models/Core/BankAccount.ts

@@ -1,9 +1,9 @@
-import { Attr, Str, Bool, Model } from '@vuex-orm/core'
+import {Str, Bool, Model, Uid} from '@vuex-orm/core'
 
 export class BankAccount extends Model {
   static entity = 'bank_accounts'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Str('', { nullable: true })

+ 2 - 2
models/Core/ContactPoint.ts

@@ -1,9 +1,9 @@
-import { Attr, Str, Model } from '@vuex-orm/core'
+import {Str, Model, Uid} from '@vuex-orm/core'
 
 export class ContactPoint extends Model {
   static entity = 'contact_points'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Str('PRINCIPAL', { nullable: false })

+ 2 - 2
models/Core/Country.ts

@@ -1,9 +1,9 @@
-import { Attr, Str, Model } from '@vuex-orm/core'
+import {Str, Model, Uid} from '@vuex-orm/core'
 
 export class Country extends Model {
   static entity = 'countries'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Str('')

+ 24 - 0
models/Core/Notification.ts

@@ -0,0 +1,24 @@
+import {Attr, Str, Model, HasOne, Uid} from '@vuex-orm/core'
+import {NotificationMessage} from "~/models/Core/NotificationMessage";
+
+export class Notification extends Model {
+  static entity = 'notifications'
+
+  @Uid()
+  id!: number | null
+
+  @Str('', { nullable: true })
+  name!: string
+
+  @HasOne(() => NotificationMessage, 'id')
+  message!: NotificationMessage | null
+
+  @Str('', { nullable: true })
+  type!: string
+
+  @Str('', { nullable: true })
+  link!: string
+
+  @Attr({})
+  notificationUsers!: Array<string>
+}

+ 17 - 0
models/Core/NotificationMessage.ts

@@ -0,0 +1,17 @@
+import {Str, Model, Uid} from '@vuex-orm/core'
+
+export class NotificationMessage extends Model {
+  static entity = 'notification_messages'
+
+  @Uid()
+  id!: number | null
+
+  @Str('', { nullable: true })
+  about!: string
+
+  @Str('', { nullable: true })
+  action!: string
+
+  @Str('', { nullable: true })
+  fileName!: string
+}

+ 17 - 0
models/Core/NotificationUsers.ts

@@ -0,0 +1,17 @@
+import { Str, Model, Bool, Uid } from '@vuex-orm/core'
+
+export class NotificationUsers extends Model {
+  static entity = 'notification_users'
+
+  @Uid()
+  id!: number | null
+
+  @Str('', { nullable: true })
+  notification!: string
+
+  @Str('', { nullable: true })
+  access!: string
+
+  @Bool(false, { nullable: false })
+  isRead!: boolean
+}

+ 2 - 2
models/Organization/Organization.ts

@@ -1,9 +1,9 @@
-import { Attr, Str, Bool, Num, Model } from '@vuex-orm/core'
+import {Attr, Str, Bool, Num, Model, Uid} from '@vuex-orm/core'
 
 export class Organization extends Model {
   static entity = 'organizations'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Attr({})

+ 2 - 2
models/Organization/OrganizationAddressPostal.ts

@@ -1,10 +1,10 @@
-import { Attr, Str, HasOne, Model } from '@vuex-orm/core'
+import {Str, HasOne, Model, Uid} from '@vuex-orm/core'
 import { AddressPostal } from '~/models/Core/AddressPostal'
 
 export class OrganizationAddressPostal extends Model {
   static entity = 'organization_address_postals'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @HasOne(() => AddressPostal, 'id')

+ 2 - 2
models/Organization/OrganizationLicence.ts

@@ -1,9 +1,9 @@
-import { Attr, Str, Model } from '@vuex-orm/core'
+import {Str, Model, Uid} from '@vuex-orm/core'
 
 export class OrganizationLicence extends Model {
   static entity = 'organization_licences'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Str('', { nullable: false })

+ 2 - 2
models/Organization/OrganizationNetwork.ts

@@ -1,9 +1,9 @@
-import { Attr, Str, Model } from '@vuex-orm/core'
+import {Str, Model, Uid} from '@vuex-orm/core'
 
 export class OrganizationLicence extends Model {
   static entity = 'organization_licences'
 
-  @Attr(null)
+  @Uid()
   id!: number | null
 
   @Str('', { nullable: false })

+ 22 - 10
services/connection/connection.ts

@@ -2,6 +2,7 @@ import {NuxtAxiosInstance} from '@nuxtjs/axios'
 import {AxiosRequestConfig} from 'axios'
 import {AnyJson, DataPersisterArgs, DataProviderArgs, UrlArgs} from '~/types/interfaces'
 import {HTTP_METHOD, QUERY_TYPE} from '~/types/enums'
+import TypesTesting from "~/services/utils/typesTesting";
 
 /**
  * @category Services/connection
@@ -37,13 +38,15 @@ class Connection {
         }
 
       case HTTP_METHOD.PUT:
-        if (!Connection.isDataPersisterArgs(args)) {
+      case HTTP_METHOD.POST:
+        if (!TypesTesting.isDataPersisterArgs(args)) {
           throw new Error('*args* is not a dataPersisterArgs')
         }
         if (!args.data) {
           throw new Error('*args* has no data')
         }
-        return Connection.put(url, args.id, args.data, args.showProgress)
+        return method === HTTP_METHOD.PUT ? Connection.put(url, args.id, args.data, args.showProgress) :
+                                            Connection.post(url, args.data, args.showProgress)
 
       case HTTP_METHOD.DELETE:
         return Connection.deleteItem(url, args.id, args.showProgress)
@@ -87,6 +90,23 @@ class Connection {
     return Connection.request(config)
   }
 
+  /**
+   * Post : préparation de la config pour la création d'un item
+   * @param {string} url
+   * @param {AnyJson} data
+   * @param {boolean} progress
+   * @return {Promise<any>}
+   */
+  public static post (url: string, data: AnyJson, progress: boolean = true): Promise<any> {
+    const config: AxiosRequestConfig = {
+      url: `${url}`,
+      method: HTTP_METHOD.POST,
+      data,
+      progress
+    }
+    return Connection.request(config)
+  }
+
   /**
    * Put : préparation de la config pour la mise à jour d'un item
    * @param {string} url
@@ -129,14 +149,6 @@ class Connection {
   public static async request (config: AxiosRequestConfig): Promise<any> {
     return await Connection.connector.$request(config)
   }
-
-  /**
-   * Test si l'argument est bien de type DataPersister
-   * @param args
-   */
-  private static isDataPersisterArgs (args: DataProviderArgs|DataPersisterArgs): args is DataPersisterArgs {
-    return (args as DataPersisterArgs).data !== undefined
-  }
 }
 
 export default Connection

+ 23 - 20
services/connection/urlBuilder.ts

@@ -1,7 +1,9 @@
-import { Model } from '@vuex-orm/core'
-import {DataPersisterArgs, DataProviderArgs, ImageArgs, UrlArgs} from '~/types/interfaces'
-import { QUERY_TYPE } from '~/types/enums'
-import { repositoryHelper } from '~/services/store/repository'
+import {Model} from '@vuex-orm/core'
+import {ImageArgs, UrlArgs} from '~/types/interfaces'
+import {QUERY_TYPE} from '~/types/enums'
+import {repositoryHelper} from '~/services/store/repository'
+import TypesTesting from "~/services/utils/typesTesting";
+import UrlOptionsBuilder from "~/services/connection/urlOptionsBuilder";
 
 /**
  * Classe permettant de construire une URL pour l'interrogation d'une API externe
@@ -10,33 +12,42 @@ class UrlBuilder {
   static ROOT = '/api/'
 
   /**
-   * Main méthode qui appellera les méthode privées correspondantes (getDefaultUrl, getEnumUrl, getModelUrl)
+   * Main méthode qui appellera les méthode privées correspondantes (getDefaultUrl, getEnumUrl, getModelUrl, getImageUrl)
    * @param {UrlArgs} args
    * @return {string}
    */
   public static build (args: UrlArgs): string {
+    let url: string = ''
     switch (args.type) {
       case QUERY_TYPE.DEFAULT:
-        return UrlBuilder.getDefaultUrl(args.url)
+        url = UrlBuilder.getDefaultUrl(args.url)
+        break;
 
       case QUERY_TYPE.ENUM:
-        return UrlBuilder.getEnumUrl(args.enumType)
+        url = UrlBuilder.getEnumUrl(args.enumType)
+        break;
 
       case QUERY_TYPE.MODEL:
-        return UrlBuilder.getModelUrl(args.model, args.rootModel, args.rootId)
+        url = UrlBuilder.getModelUrl(args.model, args.rootModel, args.rootId)
+        break;
 
       case QUERY_TYPE.IMAGE:
-        if (!UrlBuilder.isDataProviderArgs(args)) {
+        if (!TypesTesting.isDataProviderArgs(args)) {
           throw new Error('*args* is not a dataProviderArgs')
         }
         if (!args.imgArgs) {
           throw new Error('*args* has no imgArgs')
         }
-        return UrlBuilder.getImageUrl(args.imgArgs, args.baseUrl)
+        url = UrlBuilder.getImageUrl(args.imgArgs, args.baseUrl)
+        break;
 
       default:
-        throw new Error('url, model or enum must be defined')
+        throw new Error('url, model, image or enum must be defined')
+        break;
     }
+
+    const options = UrlOptionsBuilder.build(args)
+    return options.length > 0 ? `${url}?${UrlOptionsBuilder.build(args).join('&')}` : url
   }
 
   /**
@@ -99,7 +110,7 @@ class UrlBuilder {
    * @return {string}
    */
   private static getImageUrl (imgArgs: ImageArgs, baseUrl: string = ''): string {
-    const downloadUrl = `files/${imgArgs.id}/download/${imgArgs.height}x${imgArgs.width}?${new Date().getTime()}`
+    const downloadUrl = `files/${imgArgs.id}/download/${imgArgs.height}x${imgArgs.width}`
     return UrlBuilder.concat(baseUrl, UrlBuilder.ROOT, downloadUrl)
   }
 
@@ -116,14 +127,6 @@ class UrlBuilder {
     })
     return url
   }
-
-  /**
-   * Test si l'argument est bien de type DataProviderArgs
-   * @param args
-   */
-  private static isDataProviderArgs (args: DataProviderArgs|DataPersisterArgs): args is DataProviderArgs {
-    return (args as DataProviderArgs).imgArgs !== undefined
-  }
 }
 
 export default UrlBuilder

+ 57 - 0
services/connection/urlOptionsBuilder.ts

@@ -0,0 +1,57 @@
+/**
+ * Classe permettant de construire les options d'une URL
+ */
+import TypesTesting from "~/services/utils/typesTesting";
+import {ListArgs, UrlArgs} from "~/types/interfaces";
+import {QUERY_TYPE} from "~/types/enums";
+
+class UrlOptionsBuilder {
+
+  /**
+   * Main méthode qui appellera les méthode privées correspondantes (getUrlOptionsImage, getUrlOptionsLists)
+   * @param {UrlArgs} args
+   * @return {string}
+   */
+  public static build(args: UrlArgs): Array<string> {
+    let options: Array<string> = []
+
+    if (args.type === QUERY_TYPE.IMAGE){
+      options = [...options, this.getUrlOptionsImage()]
+    }
+
+    if (TypesTesting.isDataProviderArgs(args) && args.listArgs !== undefined) {
+      options = [...options, ...this.getUrlOptionsLists(args.listArgs)]
+    }
+
+    return options
+  }
+
+  /**
+   * Une image doit toujours avoir le time en options pour éviter les problème de cache
+   * @private
+   */
+  private static getUrlOptionsImage(): string {
+    return new Date().getTime().toString()
+  }
+
+  /**
+   * Fonction renvoyant le tableau d'options d'une list
+   * @param listArgs
+   * @private
+   */
+  private static getUrlOptionsLists(listArgs: ListArgs): Array<string> {
+    const options: Array<string> = []
+
+    if (listArgs.itemsPerPage) {
+      options.push(`itemsPerPage=${listArgs.itemsPerPage}`)
+    }
+
+    if (listArgs.page) {
+      options.push(`page=${listArgs.page}`)
+    }
+
+    return options
+  }
+}
+
+export default UrlOptionsBuilder

+ 1 - 1
services/data/dataProvider.ts

@@ -38,7 +38,7 @@ class DataProvider extends BaseDataManager {
     const data = await Serializer.denormalize(response, DENORMALIZER_TYPE.HYDRA)
 
     // post-process the data with the first supported processor
-    return await this.process(data, queryArguments  )
+    return await this.process(data, queryArguments)
   }
 
   /**

+ 1 - 3
services/data/processor/baseProcessor.ts

@@ -1,13 +1,11 @@
 import { Context } from '@nuxt/types/app'
 import { AnyJson, DataProviderArgs } from '~/types/interfaces'
-import Hookable from '~/services/data/hookable'
 
-class BaseProcessor extends Hookable {
+class BaseProcessor {
   protected arguments!: DataProviderArgs;
   protected ctx!: Context;
 
   constructor (ctx: Context, args: DataProviderArgs) {
-    super()
     this.arguments = args
     this.ctx = ctx
   }

+ 3 - 3
services/data/processor/enumProcessor.ts

@@ -1,5 +1,5 @@
 import * as _ from 'lodash'
-import { AnyJson, DataProviderArgs, EnumChoice, EnumChoices, Processor } from '~/types/interfaces'
+import {ApiResponse, DataProviderArgs, EnumChoice, EnumChoices, Processor} from '~/types/interfaces'
 import BaseProcessor from '~/services/data/processor/baseProcessor'
 import { QUERY_TYPE } from '~/types/enums'
 
@@ -18,10 +18,10 @@ class EnumProcessor extends BaseProcessor implements Processor {
    * @param data
    */
   // eslint-disable-next-line require-await
-  async process (data: AnyJson): Promise<any> {
+  async process (payload: ApiResponse): Promise<any> {
     const enums: EnumChoices = []
 
-    _.each(data.items, (item, key) => {
+    _.each(payload.data.items, (item, key) => {
       const entry:EnumChoice = {
         value: key,
         label: this.ctx.app.i18n.t(item) as string

+ 5 - 3
services/data/processor/imageProcessor.ts

@@ -1,4 +1,4 @@
-import {DataProviderArgs, Processor} from '~/types/interfaces'
+import {ApiResponse, DataProviderArgs, Processor} from '~/types/interfaces'
 import BaseProcessor from '~/services/data/processor/baseProcessor'
 import {QUERY_TYPE} from '~/types/enums'
 
@@ -15,8 +15,10 @@ class ImageProcessor extends BaseProcessor implements Processor {
    * @param {BlobPart[]} data
    */
   // eslint-disable-next-line require-await
-  async process(data: any): Promise<any> {
-    let blob = new Blob([data], {type: 'image/jpeg'});
+  async process(payload: ApiResponse): Promise<any> {
+    if(payload.data.size === 0) throw new Error('image not found')
+
+    let blob = new Blob([payload.data as BlobPart], {type: 'image/jpeg'});
     return await this.blobToBase64(blob);
   }
 

+ 9 - 6
services/data/processor/modelProcessor.ts

@@ -1,7 +1,7 @@
 import * as _ from 'lodash'
-import { AnyJson, DataProviderArgs, Processor } from '~/types/interfaces'
+import {ApiResponse, DataProviderArgs, Processor} from '~/types/interfaces'
 import BaseProcessor from '~/services/data/processor/baseProcessor'
-import { QUERY_TYPE } from '~/types/enums'
+import {METADATA_TYPE, QUERY_TYPE} from '~/types/enums'
 import { repositoryHelper } from '~/services/store/repository'
 
 class ModelProcessor extends BaseProcessor implements Processor {
@@ -17,15 +17,18 @@ class ModelProcessor extends BaseProcessor implements Processor {
    * Exécute la requête et retourne la réponse désérialisée
    * @param data
    */
-  async process (data: AnyJson): Promise<any> {
+  async process (payload: ApiResponse): Promise<any> {
     if (typeof this.arguments.model === 'undefined') {
       throw new TypeError('model must be defined')
     }
 
-    data.originalState = _.cloneDeep(data)
-    await repositoryHelper.persist(this.arguments.model, data)
+    if(payload.metadata.type !== METADATA_TYPE.COLLECTION){
+      payload.data.originalState = _.cloneDeep(payload)
+    }
+    // console.log(payload.data)
+    await repositoryHelper.persist(this.arguments.model, payload.data)
 
-    return data
+    return payload
   }
 }
 

+ 80 - 44
services/serializer/denormalizer/hydra.ts

@@ -1,6 +1,7 @@
-import { AnyJson } from '~/types/interfaces'
+import {AnyJson, ApiResponse, HydraMetadata} from '~/types/interfaces'
 import BaseDenormalizer from '~/services/serializer/denormalizer/baseDenormalizer'
-import { DENORMALIZER_TYPE } from '~/types/enums'
+import {DENORMALIZER_TYPE, METADATA_TYPE} from '~/types/enums'
+import {parseInt} from "lodash";
 
 /**
  * Classe permettant d'assurer la dénormalization d'un objet Hydra en JSON
@@ -25,27 +26,40 @@ class Hydra extends BaseDenormalizer {
     }
   }
 
-  /**
+  private static parseItem (hydraData: AnyJson): ApiResponse {
+    const itemResponse: ApiResponse = {
+      data: hydraData,
+      metadata: Hydra.definedMetadataForItem(hydraData)
+    }
+    return itemResponse
+  }
+    /**
    * Méthode de parsing appelé si on est dans un GET
    *
    * @param {AnyJson} hydraData
    */
-  private static parseItem (hydraData: AnyJson): AnyJson {
-    if (hydraData['hydra:previous']) {
-      const iriParts = hydraData['hydra:previous'].split('/')
-      hydraData.previous = iriParts[iriParts.length - 1]
-    }
-    if (hydraData['hydra:next']) {
-      const iriParts = hydraData['hydra:next'].split('/')
-      hydraData.next = iriParts[iriParts.length - 1]
-    }
-    if (hydraData['hydra:totalItems']) {
-      hydraData.totalItems = hydraData['hydra:totalItems']
-    }
-    if (hydraData['hydra:itemPosition']) {
-      hydraData.itemPosition = hydraData['hydra:itemPosition']
-    }
-    return hydraData
+  private static definedMetadataForItem (hydraData: AnyJson): AnyJson {
+    const metadata:HydraMetadata = {}
+
+    // if (hydraData['hydra:previous']) {
+    //   const iriParts = hydraData['hydra:previous'].split('/')
+    //   hydraData.previous = iriParts[iriParts.length - 1]
+    // }
+    // if (hydraData['hydra:next']) {
+    //   const iriParts = hydraData['hydra:next'].split('/')
+    //   hydraData.next = iriParts[iriParts.length - 1]
+    // }
+    // if (hydraData['hydra:totalItems']) {
+    //   hydraData.totalItems = hydraData['hydra:totalItems']
+    // }
+    // if (hydraData['hydra:itemPosition']) {
+    //   hydraData.itemPosition = hydraData['hydra:itemPosition']
+    // }
+
+    metadata.type = METADATA_TYPE.ITEM
+
+    return metadata
+
   }
 
   /**
@@ -53,40 +67,62 @@ class Hydra extends BaseDenormalizer {
    *
    * @param {AnyJson} hydraData
    */
-  private static parseCollection (hydraData: AnyJson): AnyJson {
-    const collectionResponse = hydraData['hydra:member']
-    collectionResponse.metadata = {}
-    collectionResponse.order = {}
-    collectionResponse.search = {}
-
-    // Put metadata in a property of the collection
-    for (const key in hydraData) {
-      const value = hydraData[key]
-      if (key !== 'hydra:member') {
-        collectionResponse.metadata[key] = value
-      }
+  private static parseCollection (hydraData: AnyJson): ApiResponse {
+    const collectionResponse:ApiResponse = {
+      data:hydraData['hydra:member'],
+      metadata : Hydra.definedMetadataForCollection(hydraData)
     }
 
+    // collectionResponse.order = {}
+    // collectionResponse.search = {}
+
+
     // Populate href property for all elements of the collection
-    for (const key in collectionResponse) {
-      const value = collectionResponse[key]
+    for (const key in collectionResponse.data) {
+      const value = collectionResponse.data[key]
       Hydra.populateAllData(value)
     }
 
-    if (typeof (hydraData['hydra:search']) !== 'undefined') {
-      const collectionSearch = hydraData['hydra:search']['hydra:mapping']
-      for (const key in collectionSearch) {
-        const value = collectionSearch[key]
-        if (value.variable.indexOf('filter[order]') === 0) {
-          collectionResponse.order[value.property] = value
-        } else if (value.variable.indexOf('filter[where]') === 0) {
-          collectionResponse.search[value.property] = value
-        }
-      }
-    }
+    // if (typeof (hydraData['hydra:search']) !== 'undefined') {
+    //   const collectionSearch = hydraData['hydra:search']['hydra:mapping']
+    //   for (const key in collectionSearch) {
+    //     const value = collectionSearch[key]
+    //     if (value.variable.indexOf('filter[order]') === 0) {
+    //       collectionResponse.order[value.property] = value
+    //     } else if (value.variable.indexOf('filter[where]') === 0) {
+    //       collectionResponse.search[value.property] = value
+    //     }
+    //   }
+    // }
+
     return collectionResponse
   }
 
+  private static  definedMetadataForCollection(data:AnyJson){
+    const metadata:HydraMetadata = {
+      totalItems: data['hydra:totalItems']
+    }
+
+    if(data['hydra:view']){
+      metadata.firstPage = Hydra.getPageNumber(data['hydra:view']['hydra:first'])
+      metadata.lastPage = Hydra.getPageNumber(data['hydra:view']['hydra:last'])
+      metadata.nextPage = Hydra.getPageNumber(data['hydra:view']['hydra:next'])
+      metadata.previousPage = Hydra.getPageNumber(data['hydra:view']['hydra:previous'])
+    }
+
+    metadata.type = METADATA_TYPE.COLLECTION
+
+    return metadata
+  }
+
+  private static  getPageNumber(uri:string):number {
+    if(uri){
+      const number = uri.split('page=').pop()
+      return number ? parseInt(number) : 0
+    }
+    return 0
+  }
+
   /**
    * Hydrate l'objet JSON de façon récursive (afin de gérer les objet nested)
    *

+ 23 - 3
services/serializer/normalizer/model.ts

@@ -1,6 +1,6 @@
 import * as _ from 'lodash'
 import BaseNormalizer from '~/services/serializer/normalizer/baseNormalizer'
-import { DataPersisterArgs } from '~/types/interfaces'
+import {AnyJson, DataPersisterArgs} from '~/types/interfaces'
 import { QUERY_TYPE } from '~/types/enums'
 import { repositoryHelper } from '~/services/store/repository'
 
@@ -24,14 +24,34 @@ class Model extends BaseNormalizer {
       throw new Error('*args* has no model attribute')
     }
 
-    const item = repositoryHelper.findItemFromModel(args.model, args.id)
+    const item = repositoryHelper.findItemFromModel(args.model, args.idTemp ? args.idTemp : args.id)
 
     if (!item || typeof item === 'undefined') {
       throw new Error('Item not found')
     }
 
-    const data = item.$toJson()
+    let data = item.$toJson()
+
+    if(Model.isPostQuery(args)) data = Model.sanitizeBeforePost(data)
+
     return _.omit(data, 'originalState')
   }
+
+  /**
+   * Return true si on est dans un POST
+   * @param args
+   */
+  public static isPostQuery(args: DataPersisterArgs): boolean{
+    return args.idTemp
+  }
+
+  /**
+   * Opération de nettoyage avant un POST
+   * @param data
+   */
+  public static sanitizeBeforePost(data:AnyJson): AnyJson{
+    data.id = null
+    return data
+  }
 }
 export default Model

+ 20 - 3
services/store/repository.ts

@@ -3,6 +3,7 @@ import { Store } from 'vuex'
 import * as _ from 'lodash'
 import { $objectProperties } from '~/services/utils/objectProperties'
 import { AnyJson } from '~/types/interfaces'
+import {OrderByVuexOrm} from "~/types/types";
 
 /**
  * Classe Wrapper pour assurer les opérations les plus communes des Repository de VuexORM
@@ -43,16 +44,26 @@ class Repository {
     return this.getRepository(model).getModel().$entity()
   }
 
+  /**
+   * Créer une entry dans le repository
+   *
+   * @param {Model} model
+   * @param {AnyJson} entry
+   */
+  public make (model: typeof Model, entry?: AnyJson): Model {
+    return this.getRepository(model).make(entry)
+  }
+
   /**
    * Persist l'entry dans le repository
    *
    * @param {Model} model
    * @param {AnyJson} entry
    */
-  public persist (model: typeof Model, entry: AnyJson): void {
+  public persist (model: typeof Model, entry: AnyJson): Model {
     if (_.isEmpty(entry)) { throw new Error('entry is empty') }
 
-    this.getRepository(model).save(entry)
+    return this.getRepository(model).save(entry)
   }
 
   /**
@@ -89,10 +100,16 @@ class Repository {
    * Récupération de la Collection du Model souhaité
    *
    * @param {Model} model
+   * @param {OrderByVuexOrm} orderBy
    * @return {Collection} la collection
    */
-  public findCollectionFromModel (model: typeof Model): Collection {
+  public findCollectionFromModel (model: typeof Model, orderBy?: OrderByVuexOrm): Collection {
     const repository = this.getRepository(model)
+    if(orderBy){
+      for(const orderKey in orderBy){
+        repository.orderBy(orderKey, orderBy[orderKey])
+      }
+    }
     return repository.all()
   }
 

+ 20 - 0
services/utils/typesTesting.ts

@@ -0,0 +1,20 @@
+import {DataPersisterArgs, DataProviderArgs} from "~/types/interfaces";
+
+export default class TypesTesting {
+  /**
+   * Test si l'argument est bien de type DataProviderArgs
+   * @param args
+   */
+  public static isDataProviderArgs (args: DataProviderArgs|DataPersisterArgs): args is DataProviderArgs {
+    return (args as DataProviderArgs).imgArgs !== undefined
+        || (args as DataProviderArgs).listArgs !== undefined
+  }
+
+  /**
+   * Test si l'argument est bien de type DataPersister
+   * @param args
+   */
+  public static isDataPersisterArgs (args: DataProviderArgs|DataPersisterArgs): args is DataPersisterArgs {
+    return (args as DataPersisterArgs).data !== undefined
+  }
+}

+ 1 - 1
store/index.js

@@ -50,6 +50,6 @@ export const actions = {
       type: QUERY_TYPE.DEFAULT,
       url: 'my_profile'
     })
-    await dispatch('profile/access/setProfile', myProfile)
+    await dispatch('profile/access/setProfile', myProfile.data)
   }
 }

+ 36 - 0
tests/unit/services/connection/urlOptionsBuilder.spec.ts

@@ -0,0 +1,36 @@
+import UrlOptionsBuilder from '~/services/connection/urlOptionsBuilder'
+import {QUERY_TYPE} from '~/types/enums'
+import {DataProviderArgs} from "~/types/interfaces";
+
+describe('build()', () => {
+  it('should return an array with options URL image at the end', () => {
+    const args: DataProviderArgs = {
+      type: QUERY_TYPE.IMAGE,
+      imgArgs: {
+        id:1,
+        height:100,
+        width:100
+      }
+    }
+
+    jest.useFakeTimers('modern');
+    jest.setSystemTime(new Date(2020, 3, 1));
+
+    const options = UrlOptionsBuilder.build(args)
+    expect(options.pop()).toEqual('1585692000000')
+  })
+
+  it('should return an array with options lists', () => {
+    const args: DataProviderArgs = {
+      type: QUERY_TYPE.MODEL,
+      listArgs: {
+        itemsPerPage:10,
+        page:1
+      }
+    }
+
+    const options = UrlOptionsBuilder.build(args)
+    expect(options.pop()).toEqual('page=1')
+    expect(options.pop()).toEqual('itemsPerPage=10')
+  })
+})

+ 21 - 20
tests/unit/services/serializer/denormalizer/hydra.spec.ts

@@ -33,25 +33,26 @@ describe('denormalize()', () => {
     }
 
     expect(Hydra.denormalize(serverResponse)).toStrictEqual<AnyJson>({
-      '@context': '/api/contexts/Access',
-      '@id': '/api/accesses/7351',
-      '@type': 'Access',
-      'hydra:itemPosition': 1,
-      'hydra:next': '/api/organizations?page=2',
-      'hydra:previous': '/api/organizations?page=1',
-      'hydra:totalItems': 20,
-      id: 7351,
-      itemPosition: 1,
-      next: 'organizations?page=2',
-      organization: '/api/organizations/37306',
-      person: {
-        '@type': 'Person',
-        givenName: 'Patrick',
-        id: 11344,
-        name: 'BRUEL'
+      "data": {
+        "@context": "/api/contexts/Access",
+        "@id": "/api/accesses/7351",
+        "@type": "Access",
+        "hydra:itemPosition": 1,
+        "hydra:next": "/api/organizations?page=2",
+        "hydra:previous": "/api/organizations?page=1",
+        "hydra:totalItems": 20,
+        "id": 7351,
+        "organization": "/api/organizations/37306",
+        "person": {
+          "@type": "Person",
+          "givenName": "Patrick",
+          "id": 11344,
+          "name": "BRUEL"
+        }
       },
-      previous: 'organizations?page=1',
-      totalItems: 20
+      "metadata": {
+        "type": 0
+      }
     })
   })
 
@@ -99,7 +100,7 @@ describe('denormalize()', () => {
     }
 
     const response = Hydra.denormalize(serverResponse)
-    expect(JSON.stringify(response)).toEqual(JSON.stringify([
+    expect(JSON.stringify(response)).toEqual(JSON.stringify({"data":[
       {
         '@id': '/api/accesses/7351',
         organization: '/api/organizations/37306',
@@ -124,7 +125,7 @@ describe('denormalize()', () => {
             givenName: 'George'
           }
       }
-    ]
+    ], 'metadata':{'type':1}}
     ))
   })
 })

+ 0 - 8
tests/unit/services/utils/apiError.spec.ts

@@ -1,8 +0,0 @@
-import ApiError from '~/services/exception/apiError'
-
-describe('getStatus()', () => {
-  it('should return the status code', () => {
-    const apiError = new ApiError(404, 'not found')
-    expect(apiError.getStatus()).toEqual(404)
-  })
-})

+ 47 - 0
tests/unit/services/utils/typesTesting.spec.ts

@@ -0,0 +1,47 @@
+import {DataPersisterArgs, DataProviderArgs} from "~/types/interfaces";
+import {QUERY_TYPE} from "~/types/enums";
+import TypesTesting from "~/services/utils/typesTesting";
+
+describe('isDataProviderArgs()', () => {
+  it('should return true if data is DataProviderArgs', () => {
+    const args:DataProviderArgs = {
+      type: QUERY_TYPE.MODEL,
+      listArgs: {
+        itemsPerPage: 10,
+        page: 1
+      }
+    }
+    expect(TypesTesting.isDataProviderArgs(args)).toBeTruthy()
+  })
+
+  it('should return false if data is not a DataProviderArgs', () => {
+    const args:DataPersisterArgs = {
+      type: QUERY_TYPE.MODEL,
+      data: {
+      }
+    }
+    expect(TypesTesting.isDataProviderArgs(args)).toBeFalsy()
+  })
+})
+
+describe('isDataPersisterArgs()', () => {
+  it('should return true if data is DataPersisterArgs', () => {
+    const args:DataPersisterArgs = {
+      type: QUERY_TYPE.MODEL,
+      data: {
+      }
+    }
+    expect(TypesTesting.isDataPersisterArgs(args)).toBeTruthy()
+  })
+
+  it('should return false if data is not a DataPersisterArgs', () => {
+    const args:DataProviderArgs = {
+      type: QUERY_TYPE.MODEL,
+      listArgs: {
+        itemsPerPage: 10,
+        page: 1
+      }
+    }
+    expect(TypesTesting.isDataPersisterArgs(args)).toBeFalsy()
+  })
+})

+ 11 - 0
types/enums.ts

@@ -33,3 +33,14 @@ export const enum GENDER {
   MISTER = 'MISTER',
   MISS = 'MISS'
 }
+
+export const enum METADATA_TYPE {
+  ITEM,
+  COLLECTION
+}
+
+export const enum NOTIFICATION_TYPE {
+  MESSAGE= 'MESSAGE',
+  FILE= 'FILE'
+}
+

+ 25 - 5
types/interfaces.d.ts

@@ -5,7 +5,7 @@ import { Context } from '@nuxt/types/app'
 import DataPersister from '~/services/data/dataPersister'
 import DataProvider from '~/services/data/dataProvider'
 import DataDeleter from '~/services/data/dataDeleter'
-import { ABILITIES, GENDER, QUERY_TYPE, TYPE_ALERT } from '~/types/enums'
+import {ABILITIES, GENDER, METADATA_TYPE, QUERY_TYPE, TYPE_ALERT} from '~/types/enums'
 
 /**
  * Upgrade du @nuxt/types pour TypeScript
@@ -144,9 +144,9 @@ interface UrlArgs {
   readonly model?: typeof Model,
   readonly rootModel?: typeof Model,
   readonly id?: any,
-  readonly rootId?: number
-  readonly showProgress?: boolean
-
+  readonly idTemp?: any,
+  readonly rootId?: number,
+  readonly showProgress?: boolean,
   readonly hook?: string
 }
 
@@ -156,8 +156,14 @@ interface ImageArgs {
   readonly width: number
 }
 
+interface ListArgs {
+  readonly itemsPerPage: number,
+  readonly page: number
+}
+
 interface DataProviderArgs extends UrlArgs {
-  imgArgs?: ImageArgs
+  imgArgs?: ImageArgs,
+  listArgs?: ListArgs,
 }
 interface DataPersisterArgs extends UrlArgs {
   data?: AnyJson
@@ -233,3 +239,17 @@ interface MobytUserStatus {
   amount: number,
   money: number
 }
+
+interface ApiResponse{
+  data: AnyJson,
+  metadata: HydraMetadata
+}
+
+interface HydraMetadata {
+  readonly totalItems?: number,
+  firstPage?: number,
+  lastPage?: number,
+  nextPage?: number,
+  previousPage?: number,
+  type?: METADATA_TYPE
+}

+ 5 - 1
types/types.d.ts

@@ -1,3 +1,7 @@
-import { Query, Repository } from '@vuex-orm/core'
+import {OrderBy, OrderDirection, Query, Repository} from '@vuex-orm/core'
 
 export type RepositoryOrQuery<R extends Repository = Repository, Q extends Query = Query> = R | Q;
+
+interface OrderByVuexOrm {
+  [key: string]: OrderDirection;
+}