Browse Source

Merge branch 'feature/BTTF-58' into develop

Vincent GUFFON 4 years ago
parent
commit
76b89d1b5a

BIN
assets/images/byDefault/men-1.png


BIN
assets/images/byDefault/women-1.png


BIN
assets/vue_js_logo.png


+ 1 - 0
components/Layout/Header.vue

@@ -38,6 +38,7 @@ et aux préférences de l'utilisateur
       <template #activator="{ on, attrs }">
         <v-btn
           icon
+          class="ml-2"
           v-bind="attrs"
           v-on="on"
         >

+ 64 - 37
components/Layout/Header/Menu.vue

@@ -4,32 +4,25 @@ header principal (configuration, paramètres du compte...)
 -->
 
 <template>
-  <v-menu offset-y left max-height="300">
+  <v-menu offset-y left>
     <template v-slot:activator="{ on: { click }, attrs }">
       <v-tooltip bottom>
         <template v-slot:activator="{ on: on_tooltips , attrs: attrs_tooltips }">
-          <v-avatar
-            v-if="avatar"
-            size="30"
-            v-bind="[attrs, attrs_tooltips]"
-            v-on="on_tooltips"
-            @click="click"
-          >
-            <img
-              src="https://cdn.vuetifyjs.com/images/john.jpg"
-              alt="John"
-            >
-          </v-avatar>
-
           <v-btn
-            v-else
+
             icon
             v-bind="[attrs, attrs_tooltips]"
             color=""
             v-on="on_tooltips"
             @click="click"
           >
-            <v-icon class="ot_white--text" small>
+            <v-avatar
+              v-if="avatar"
+              size="30"
+            >
+              <UiImage :id="avatarId" :imageByDefault="avatarByDefault" :width="30"></UiImage>
+            </v-avatar>
+            <v-icon  v-else class="ot_white--text" small>
               {{ menu.icon }}
             </v-icon>
           </v-btn>
@@ -37,31 +30,51 @@ header principal (configuration, paramètres du compte...)
         <span>{{ $t(menu.title) }}</span>
       </v-tooltip>
     </template>
-    <v-list dense :subheader="true">
-      <v-list-item dense class="ot_light_grey">
-        <v-list-item-title v-text="$t(menu.title)" />
-      </v-list-item>
-      <template v-for="(item, index) in menu.children">
-        <v-list-item
-          :key="item.title"
-          :href="item.isExternalLink ? item.to : undefined"
-          :to="!item.isExternalLink ? item.to : undefined"
-          router
-          exact
-        >
-          <v-list-item-title v-text="$t(item.title)" />
-        </v-list-item>
-        <v-divider
-          v-if="index < menu.length - 1"
-          :key="index"
-        />
-      </template>
-    </v-list>
+    <v-card scrollable>
+      <v-card-title class="ot_super_light_grey 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-list dense :subheader="true">
+          <template v-for="(item, index) in menu.children">
+            <v-list-item
+
+              :id="item.title"
+              :key="item.title"
+              :href="item.isExternalLink ? item.to : undefined"
+              :to="!item.isExternalLink ? item.to : undefined"
+              router
+              exact
+            >
+              <v-list-item-title v-text="$t(item.title)"/>
+            </v-list-item>
+          </template>
+        </v-list>
+      </v-card-text>
+      <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"
+            :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>
+        </template>
+      </v-card-actions>
+    </v-card>
+
+
   </v-menu>
 </template>
 
 <script lang="ts">
-import { defineComponent } from '@nuxtjs/composition-api'
+import {defineComponent, useStore} from '@nuxtjs/composition-api'
+import {accessState} from "~/types/interfaces";
+import {Store} from "vuex";
 
 export default defineComponent({
   props: {
@@ -73,6 +86,20 @@ export default defineComponent({
       type: Boolean,
       required: false
     }
+  },
+  setup () {
+    const store:Store<any> = useStore()
+    const accessStore: accessState = store.state.profile.access
+    return {
+      avatarId: accessStore.avatarId,
+      avatarByDefault: accessStore.gender == 'MISTER' ? 'men-1.png' : 'women-1.png'
+    }
   }
 })
 </script>
+<style scoped>
+  #logout{
+    background: var(--v-ot_green-base, white);
+    color: white;
+  }
+</style>

+ 2 - 0
components/Layout/SubHeader/PersonnalizedList.vue

@@ -44,6 +44,7 @@ import { PersonalizedList } from '~/models/Access/PersonalizedList'
 import { repositoryHelper } from '~/services/store/repository'
 import { AnyJson } from '~/types/interfaces'
 import { $objectProperties } from '~/services/utils/objectProperties'
+import VueI18n from "vue-i18n";
 
 export default defineComponent({
   fetchOnServer: false,
@@ -85,6 +86,7 @@ export default defineComponent({
 /**
    * On regroupe la list par clé afin de constituer les groups
    * @param lists
+   * @param i18n
    */
 function groupListByKey (lists:Collection<PersonalizedList>): Array<AnyJson> {
   const { app: { i18n } } = useContext()

+ 80 - 0
components/Ui/Image.vue

@@ -0,0 +1,80 @@
+<template>
+  <main>
+    <v-img
+      :src="imageLoaded"
+      :lazy-src="require(`/assets/images/byDefault/${imageByDefault}`)"
+      :min-height="height"
+      :min-width="width"
+      aspect-ratio="1"
+    >
+      <template v-slot:placeholder>
+        <v-row
+          class="fill-height ma-0"
+          align="center"
+          justify="center"
+        >
+          <v-progress-circular
+            indeterminate
+            color="grey lighten-1"
+          ></v-progress-circular>
+        </v-row>
+      </template>
+    </v-img>
+  </main>
+</template>
+
+
+<script lang="ts">
+import {defineComponent, ref, Ref, useContext, useFetch} from '@nuxtjs/composition-api'
+import {QUERY_TYPE} from "~/types/enums";
+
+export default defineComponent({
+  props: {
+    id: {
+      type: Number,
+      required: true
+    },
+    imageByDefault: {
+      type: String,
+      required: false,
+      default: 'default_pic.jpeg'
+    },
+    height: {
+      type: Number,
+      required: false,
+      default: 0
+    },
+    width: {
+      type: Number,
+      required: false,
+      default: 0
+    }
+  },
+  fetchOnServer: false,
+  setup(props) {
+    const {$dataProvider, $config} = useContext()
+    const imageLoaded: Ref<String> = ref('')
+
+    useFetch(async () => {
+        try{
+          imageLoaded.value = await $dataProvider.invoke({
+            type: QUERY_TYPE.IMAGE,
+            baseUrl: $config.baseURL_Legacy,
+            imgArgs: {
+              id: props.id,
+              height: props.height,
+              width: props.width
+            }
+          })
+        }catch (e){
+          imageLoaded.value = require(`/assets/images/byDefault/${props.imageByDefault}`)
+        }
+      }
+    )
+
+    return {
+      imageLoaded
+    }
+  }
+})
+</script>

+ 1 - 1
components/Ui/Input/Autocomplete.vue

@@ -28,7 +28,7 @@ export default defineComponent({
     label: {
       type: String,
       required: false,
-      default: ''
+      default: null
     },
     field: {
       type: String,

+ 1 - 1
components/Ui/Input/AutocompleteWithAPI.vue

@@ -35,7 +35,7 @@ export default defineComponent({
     label: {
       type: String,
       required: false,
-      default: ''
+      default: null
     },
     field: {
       type: String,

+ 1 - 1
components/Ui/Input/Checkbox.vue

@@ -31,7 +31,7 @@ export default defineComponent({
     label: {
       type: String,
       required: false,
-      default: ''
+      default: null
     },
     data: {
       type: Boolean,

+ 1 - 1
components/Ui/Input/DatePicker.vue

@@ -53,7 +53,7 @@ export default defineComponent({
     label: {
       type: String,
       required: false,
-      default: ''
+      default: null
     },
     data: {
       type: [String, Array],

+ 1 - 1
components/Ui/Input/Enum.vue

@@ -40,7 +40,7 @@ export default defineComponent({
     label: {
       type: String,
       required: false,
-      default: ''
+      default: null
     },
     field: {
       type: String,

+ 1 - 1
components/Ui/Input/Text.vue

@@ -26,7 +26,7 @@ export default defineComponent({
     label: {
       type: String,
       required: false,
-      default: ''
+      default: null
     },
     field: {
       type: String,

+ 1 - 1
config/nuxtConfig/build.js

@@ -2,7 +2,7 @@ import webpack from 'webpack'
 
 export default {
   // Enable server-side rendering (https://go.nuxtjs.dev/ssr-mode)
-  ssr: true,
+  target: 'server',
 
   // Auto import components (https://go.nuxtjs.dev/config-components)
   components: true,

+ 1 - 0
jest.config.js

@@ -1,4 +1,5 @@
 module.exports = {
+  resetMocks: true,
   preset: '@nuxt/test-utils',
   moduleNameMapper: {
     '^@/(.*)$': '<rootDir>/$1',

+ 1 - 1
layouts/default.vue

@@ -1,7 +1,7 @@
 <template>
   <main>
     <!-- The client only is used to show the loading picture (@see https://nuxtjs.org/docs/2.x/features/nuxt-components#the-client-only-component) -->
-    <client-only placeholder="Loading..." />
+    <client-only placeholder=" " />
 
     <v-app dark>
       <LayoutMenu v-if="displayedMenu" :menu="menu" :mini-variant="properties.miniVariant" />

+ 4 - 4
package.json

@@ -5,7 +5,7 @@
   "scripts": {
     "dev": "nuxt --hostname '0.0.0.0' --port 3002",
     "build": "nuxt build",
-    "start": "nuxt start",
+    "start": "nuxt start --hostname '0.0.0.0' --port 3002",
     "generate": "nuxt generate",
     "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .",
     "lint": "yarn lint:js",
@@ -17,12 +17,12 @@
     "@casl/vue": "^1.2",
     "@fortawesome/fontawesome-free": "^5.15",
     "@nuxt/components": "^2.2",
+    "@nuxt/image": "^0.6.0",
     "@nuxt/typescript-runtime": "^2.0",
     "@nuxtjs/axios": "^5.13",
-    "@nuxtjs/vuetify": "^1.12.1",
-    "@nuxtjs/composition-api": "^0.28",
+    "@nuxtjs/composition-api": "^0.29.3",
     "@nuxtjs/i18n": "^7.0",
-    "@nuxt/image": "^0.6.0",
+    "@nuxtjs/vuetify": "^1.12.1",
     "@types/lodash": "^4.14",
     "@vuex-orm/core": "1.0.0-draft.14",
     "cookieparser": "^0.1",

+ 0 - 2
pages/subscription.vue

@@ -289,8 +289,6 @@ export default defineComponent({
     const { store } = useContext()
     const organizationProfile = $organizationProfile(store)
 
-    console.log(organizationProfile)
-
     return {
       organizationProfile
     }

+ 12 - 8
services/connection/connection.ts

@@ -1,7 +1,7 @@
-import { NuxtAxiosInstance } from '@nuxtjs/axios'
-import { AxiosRequestConfig } from 'axios'
-import { AnyJson, DataPersisterArgs, DataProviderArgs, UrlArgs } from '~/types/interfaces'
-import { HTTP_METHOD } from '~/types/enums'
+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'
 
 /**
  * @category Services/connection
@@ -33,7 +33,7 @@ class Connection {
         if (args.id) {
           return Connection.getItem(url, args.id, args.showProgress)
         } else {
-          return Connection.getCollection(url, args.showProgress)
+          return Connection.getCollection(url, args.showProgress, args.type)
         }
 
       case HTTP_METHOD.PUT:
@@ -72,14 +72,18 @@ class Connection {
    * Get collection : préparation de la config pour la récupération d'une collection d'items
    * @param {string} url
    * @param {boolean} progress
+   * @param {QUERY_TYPE} type
    * @return {Promise<any>}
    */
-  public static getCollection (url: string, progress: boolean = true): Promise<any> {
-    const config: AxiosRequestConfig = {
+  public static getCollection (url: string, progress: boolean = true, type: QUERY_TYPE): Promise<any> {
+    let config: AxiosRequestConfig = {
       url: `${url}`,
       method: HTTP_METHOD.GET,
       progress
     }
+    if(type === QUERY_TYPE.IMAGE)
+      config = {...config, responseType: 'blob'}
+
     return Connection.request(config)
   }
 
@@ -122,7 +126,7 @@ class Connection {
    * @param {AxiosRequestConfig} config
    * @return {Promise<any>}
    */
-  private static async request (config: AxiosRequestConfig): Promise<any> {
+  public static async request (config: AxiosRequestConfig): Promise<any> {
     return await Connection.connector.$request(config)
   }
 

+ 30 - 1
services/connection/urlBuilder.ts

@@ -1,5 +1,5 @@
 import { Model } from '@vuex-orm/core'
-import { UrlArgs } from '~/types/interfaces'
+import {DataPersisterArgs, DataProviderArgs, ImageArgs, UrlArgs} from '~/types/interfaces'
 import { QUERY_TYPE } from '~/types/enums'
 import { repositoryHelper } from '~/services/store/repository'
 
@@ -25,6 +25,15 @@ class UrlBuilder {
       case QUERY_TYPE.MODEL:
         return UrlBuilder.getModelUrl(args.model, args.rootModel, args.rootId)
 
+      case QUERY_TYPE.IMAGE:
+        if (!UrlBuilder.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)
+
       default:
         throw new Error('url, model or enum must be defined')
     }
@@ -82,6 +91,18 @@ class UrlBuilder {
     return UrlBuilder.concat(UrlBuilder.ROOT, entity)
   }
 
+  /**
+   * Construction d'une URL "image" qui ira concaténer l'id de l'image à downloeader passé en paramètre avec la ROOT Url définie
+   * @param {number} id
+   * @param {ImageArgs} imgArgs
+   * @param {string} baseUrl
+   * @return {string}
+   */
+  private static getImageUrl (imgArgs: ImageArgs, baseUrl: string = ''): string {
+    const downloadUrl = `files/${imgArgs.id}/download/${imgArgs.height}x${imgArgs.width}?${new Date().getTime()}`
+    return UrlBuilder.concat(baseUrl, UrlBuilder.ROOT, downloadUrl)
+  }
+
   /**
    * Concatenate a base url and a tail
    * @param base
@@ -95,6 +116,14 @@ 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

+ 13 - 14
services/data/baseDataManager.ts

@@ -1,5 +1,5 @@
 import { Context } from '@nuxt/types/app'
-import { DataManager, UrlArgs } from '~/types/interfaces'
+import {DataManager, DataPersisterArgs, DataProviderArgs, UrlArgs} from '~/types/interfaces'
 import Connection from '~/services/connection/connection'
 import Hookable from '~/services/data/hookable'
 import { HTTP_METHOD, QUERY_TYPE } from '~/types/enums'
@@ -10,7 +10,6 @@ import ApiError from '~/services/exception/apiError'
  */
 abstract class BaseDataManager extends Hookable implements DataManager {
   protected ctx!: Context
-  protected arguments!: UrlArgs
   protected defaultArguments: object = {
     type: QUERY_TYPE.MODEL,
     showProgress: true
@@ -18,7 +17,7 @@ abstract class BaseDataManager extends Hookable implements DataManager {
 
   /**
    * Initialise le contexte (la connection en particulier)
-   * @param ctx
+   * @param {Context} ctx
    */
   public initCtx (ctx: Context) {
     Connection.initConnector(ctx.$axios)
@@ -27,7 +26,7 @@ abstract class BaseDataManager extends Hookable implements DataManager {
 
   /**
    * Exécute la requête et retourne la réponse désérialisée
-   * @param _args
+   * @param {UrlArgs} _args
    */
   // eslint-disable-next-line require-await
   protected async _invoke (_args: UrlArgs): Promise<any> {
@@ -36,16 +35,16 @@ abstract class BaseDataManager extends Hookable implements DataManager {
 
   /**
    * Exécute la requête
-   * @param args
+   * @param {DataProviderArgs|DataPersisterArgs} args
    */
-  public async invoke (args: UrlArgs): Promise<any> {
-    this.arguments = { ...this.defaultArguments, ...args }
-    BaseDataManager.startLoading(this.arguments)
+  public async invoke (args: DataProviderArgs|DataPersisterArgs): Promise<any> {
+    const queryArguments = { ...this.defaultArguments, ...args }
+    BaseDataManager.startLoading(queryArguments)
 
-    await this.triggerHooks(args)
+    await this.triggerHooks(queryArguments)
 
     try {
-      return this._invoke(args)
+      return this._invoke(queryArguments)
     } catch (error) {
       throw new ApiError(error.response.status, error.response.data.detail)
     }
@@ -53,7 +52,7 @@ abstract class BaseDataManager extends Hookable implements DataManager {
 
   /**
    * Signale le début du chargement à nuxt (si showProgress est true)
-   * @param args
+   * @param {UrlArgs} args
    */
   private static startLoading (args: UrlArgs) {
     if (args.showProgress) {
@@ -64,9 +63,9 @@ abstract class BaseDataManager extends Hookable implements DataManager {
 
   /**
    * Send the request trough the Connection
-   * @param url
-   * @param method
-   * @param args
+   * @param {string} url
+   * @param {HTTP_METHOD} method
+   * @param {UrlArgs} args
    */
   protected static request (url: string, method: HTTP_METHOD, args: UrlArgs): Promise<any> {
     return Connection.invoke(method, url, args)

+ 6 - 6
services/data/dataDeleter.ts

@@ -10,22 +10,22 @@ import Connection from '~/services/connection/connection'
  * Le DataDeleter a pour rôle de supprimer des enregistrements via l'API
  */
 class DataDeleter extends BaseDataManager {
-  protected arguments!: DataDeleterArgs
   protected hooks = hooksDeleter;
 
   /**
    * Exécute la requête
+   * @param {DataDeleterArgs} queryArguments
    */
-  protected async _invoke (): Promise<any> {
+  protected async _invoke (queryArguments: DataDeleterArgs): Promise<any> {
     // build the url according to the url args
-    const url = UrlBuilder.build(this.arguments)
+    const url = UrlBuilder.build(queryArguments)
 
     // send the DELETE request to the api
-    await Connection.invoke(HTTP_METHOD.DELETE, url, this.arguments)
+    await Connection.invoke(HTTP_METHOD.DELETE, url, queryArguments)
 
     // update the store
-    if (this.arguments.model) {
-      await repositoryHelper.deleteItem(this.arguments.model, this.arguments.id)
+    if (queryArguments.model) {
+      await repositoryHelper.deleteItem(queryArguments.model, queryArguments.id)
     }
   }
 }

+ 9 - 9
services/data/dataPersister.ts

@@ -14,35 +14,35 @@ import { hooksPersister } from '~/services/data/hooks/hookPersister/_import'
  * réponse de l'API et la retourner.
  */
 class DataPersister extends BaseDataManager {
-  protected arguments!: DataPersisterArgs
   protected hooks = hooksPersister;
 
   /**
    * Exécute la requête et retourne la réponse désérialisée
+   * @param {DataDeleterArgs} queryArguments
    */
-  protected async _invoke (): Promise<any> {
+  protected async _invoke (queryArguments: DataPersisterArgs): Promise<any> {
     // serialize the data to persist and attach it to the request arguments
-    this.arguments.data = Serializer.normalize(this.arguments)
+    queryArguments.data = Serializer.normalize(queryArguments)
 
     // build the url according to the url args
-    const url = UrlBuilder.build(this.arguments)
+    const url = UrlBuilder.build(queryArguments)
 
     // send the POST / PUT request to the api and retrieve the response
     const response = await DataPersister.request(
       url,
-      this.arguments.id ? HTTP_METHOD.PUT : HTTP_METHOD.POST,
-      this.arguments
+      queryArguments.id ? HTTP_METHOD.PUT : HTTP_METHOD.POST,
+      queryArguments
     )
 
     // Deserialize, post-process and return the response
-    return await this.provideResponse(response, this.arguments)
+    return await this.provideResponse(response, queryArguments)
   }
 
   /**
    * Use a data provider to deserialize, post-process and return the response
    *
-   * @param response
-   * @param args
+   * @param {AnyJson} response
+   * @param {DataPersisterArgs} args
    */
   private async provideResponse (response: AnyJson, args: DataPersisterArgs) {
 

+ 9 - 8
services/data/dataProvider.ts

@@ -17,7 +17,6 @@ import { hooksProvider } from '~/services/data/hooks/hookProvider/_import'
  */
 class DataProvider extends BaseDataManager {
   protected progress: boolean = false
-  protected arguments!: DataProviderArgs
   protected hooks = hooksProvider;
   protected defaultArguments: object = {
     type: QUERY_TYPE.MODEL,
@@ -26,35 +25,37 @@ class DataProvider extends BaseDataManager {
 
   /**
    * Exécute la requête et retourne la réponse désérialisée
+   * @param {DataDeleterArgs} queryArguments
    */
-  protected async _invoke (): Promise<any> {
+  protected async _invoke (queryArguments:DataProviderArgs): Promise<any> {
     // build the url according to the url args
-    const url = UrlBuilder.build(this.arguments)
+    const url = UrlBuilder.build(queryArguments)
 
     // send the GET request to the api and retrieve the response
-    const response = await DataProvider.request(url, HTTP_METHOD.GET, this.arguments)
+    const response = await DataProvider.request(url, HTTP_METHOD.GET, queryArguments)
 
     // deserialize the response
     const data = await Serializer.denormalize(response, DENORMALIZER_TYPE.HYDRA)
 
     // post-process the data with the first supported processor
-    return await this.process(data, this.arguments)
+    return await this.process(data, queryArguments  )
   }
 
   /**
    * Find the first supported post data processor, and process the data with it
-   * @param data
-   * @param args
+   * @param {AnyJson} data
+   * @param {DataProviderArgs} args
    */
   public async process (data: AnyJson, args: DataProviderArgs) {
     const postProcessor = this.getProcessor(args)
+    if(!postProcessor) throw new Error('A processor need to be defined')
     return await postProcessor.process(data)
   }
 
   /**
    * Iterate over the available data processors and return an instance of
    * the first one that support the given args
-   * @param args
+   * @param {DataProviderArgs} args
    */
   private getProcessor (args: DataProviderArgs): any {
     for (const processor of processors) {

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

@@ -1,9 +1,11 @@
 import ModelProcessor from '~/services/data/processor/modelProcessor'
 import EnumProcessor from '~/services/data/processor/enumProcessor'
 import DefaultProcessor from '~/services/data/processor/defaultProcessor'
+import ImageProcessor from "~/services/data/processor/imageProcessor";
 
 export const processors = [
   DefaultProcessor,
   ModelProcessor,
-  EnumProcessor
+  EnumProcessor,
+  ImageProcessor
 ]

+ 36 - 0
services/data/processor/imageProcessor.ts

@@ -0,0 +1,36 @@
+import {DataProviderArgs, Processor} from '~/types/interfaces'
+import BaseProcessor from '~/services/data/processor/baseProcessor'
+import {QUERY_TYPE} from '~/types/enums'
+
+class ImageProcessor extends BaseProcessor implements Processor {
+  /**
+   * @param {DataProviderArgs} args
+   */
+  public static support(args: DataProviderArgs): boolean {
+    return args.type === QUERY_TYPE.IMAGE
+  }
+
+  /**
+   * proccessing des data
+   * @param {BlobPart[]} data
+   */
+  // eslint-disable-next-line require-await
+  async process(data: any): Promise<any> {
+    let blob = new Blob([data], {type: 'image/jpeg'});
+    return await this.blobToBase64(blob);
+  }
+
+  /**
+   * Transforme un Blob en Base64
+   * @param {Blob} blob
+   */
+  async blobToBase64(blob: Blob) {
+    return new Promise((resolve, _) => {
+      const reader = new FileReader();
+      reader.onloadend = () => resolve(reader.result);
+      reader.readAsDataURL(blob);
+    });
+  }
+}
+
+export default ImageProcessor

+ 28 - 8
tests/unit/services/connection/connection.spec.ts

@@ -5,34 +5,54 @@ import { HTTP_METHOD, QUERY_TYPE } from '~/types/enums'
 import { DataPersisterArgs } from '~/types/interfaces'
 
 const axiosMock = axios as jest.Mocked<NuxtAxiosInstance>
+const mockFn = jest.fn();
 
 beforeAll(() => {
   Connection.initConnector(axiosMock)
 })
 
 describe('invoke()', () => {
+
   describe('getItem()', () => {
     it('should return item data', async () => {
-      Connection.connector.$request = jest.fn().mockReturnValue({ data: 'data user 1' })
+      Connection.connector.$request = mockFn.mockReturnValue({ data: 'data user 1' })
       const response = await Connection.invoke(HTTP_METHOD.GET, 'users', { type: QUERY_TYPE.MODEL })
       expect(response).toStrictEqual({ data: 'data user 1' })
     })
 
     it('should call getItem', async () => {
-      Connection.getItem = jest.fn().mockReturnValue({})
+      Connection.getItem = mockFn.mockReturnValue({})
       await Connection.invoke(HTTP_METHOD.GET, 'users', { type: QUERY_TYPE.MODEL, id: 1 })
       expect(Connection.getItem).toHaveBeenCalled()
     })
   })
 
+  describe('getCollection() for Image type', () => {
+    it('should call getCollection with a specific config', async () => {
+      Connection.request = mockFn.mockReturnValue({})
+
+      await Connection.invoke(HTTP_METHOD.GET, 'files/1/download', { showProgress: false, type: QUERY_TYPE.IMAGE })
+
+      expect(Connection.request).toHaveBeenCalled()
+
+      expect(Connection.request).toBeCalledWith({
+          'method': HTTP_METHOD.GET,
+          'progress': false,
+          'responseType': 'blob',
+          'url': 'files/1/download'
+        }
+      )
+    })
+  })
+
   describe('getCollection()', () => {
     it('should return collection data', async () => {
-      Connection.connector.$request = jest.fn().mockReturnValue([{ data: 'data user 1' }, { data: 'data user 2' }])
+      Connection.connector.$request = mockFn.mockReturnValue([{ data: 'data user 1' }, { data: 'data user 2' }])
       const response = await Connection.invoke(HTTP_METHOD.GET, 'users', { type: QUERY_TYPE.MODEL })
       expect(response).toStrictEqual([{ data: 'data user 1' }, { data: 'data user 2' }])
     })
     it('should call getCollection and return collection data', async () => {
-      Connection.getCollection = jest.fn().mockReturnValue({})
+      Connection.getCollection = mockFn.mockReturnValue({})
       await Connection.invoke(HTTP_METHOD.GET, 'users', { type: QUERY_TYPE.MODEL })
       expect(Connection.getCollection).toHaveBeenCalled()
     })
@@ -44,13 +64,13 @@ describe('invoke()', () => {
     })
 
     it('should return item data', async () => {
-      Connection.connector.$request = jest.fn().mockReturnValue({ data: 'data user 1' })
+      Connection.connector.$request = mockFn.mockReturnValue({ data: 'data user 1' })
       const response = await Connection.invoke(HTTP_METHOD.PUT, 'users', { type: QUERY_TYPE.MODEL, id: 1, data: {} } as DataPersisterArgs)
       expect(response).toStrictEqual({ data: 'data user 1' })
     })
 
     it('should call put and return item data', async () => {
-      Connection.put = jest.fn().mockReturnValue({})
+      Connection.put = mockFn.mockReturnValue({})
       await Connection.invoke(HTTP_METHOD.PUT, 'users', { type: QUERY_TYPE.MODEL, id: 1, data: {} } as DataPersisterArgs)
       expect(Connection.put).toHaveBeenCalled()
     })
@@ -58,13 +78,13 @@ describe('invoke()', () => {
 
   describe('deleteItem()', () => {
     it('should delete item', async () => {
-      Connection.connector.$request = jest.fn().mockReturnValue({})
+      Connection.connector.$request = mockFn.mockReturnValue({})
       const response = await Connection.invoke(HTTP_METHOD.DELETE, 'users', { type: QUERY_TYPE.MODEL, id: 1 })
       expect(response).toStrictEqual({})
     })
 
     it('should call deleteItem', async () => {
-      Connection.deleteItem = jest.fn().mockReturnValue({})
+      Connection.deleteItem = mockFn.mockReturnValue({})
       await Connection.invoke(HTTP_METHOD.DELETE, 'users', { type: QUERY_TYPE.MODEL, id: 1 })
       expect(Connection.deleteItem).toHaveBeenCalled()
     })

+ 24 - 0
tests/unit/services/connection/urlBuilder.spec.ts

@@ -3,6 +3,7 @@ import { QUERY_TYPE } from '~/types/enums'
 import User from '~/tests/unit/fixture/models/User'
 import Organization from '~/tests/unit/fixture/models/Organization'
 import { repositoryHelper } from '~/services/store/repository'
+import {DataProviderArgs} from "~/types/interfaces";
 
 describe('invoke()', () => {
   describe('getDefaultUrl()', () => {
@@ -90,4 +91,27 @@ describe('invoke()', () => {
       expect(UrlBuilder.concat('/api')).toEqual('/api')
     })
   })
+
+  describe('getImageUrl()', () => {
+    it('should throw an error if imgArgs is missing', () => {
+      expect(() => UrlBuilder.build({
+        type: QUERY_TYPE.IMAGE
+      })).toThrow()
+    })
+
+    it('should return the File download URL concat with Root URL', () => {
+      const args: DataProviderArgs = {
+        type: QUERY_TYPE.IMAGE,
+        imgArgs: {
+          id: 1,
+          height: 50,
+          width: 50
+        }
+      }
+      jest.useFakeTimers('modern');
+      jest.setSystemTime(new Date(2020, 3, 1));
+      expect(UrlBuilder.build(args)).toEqual('/api/files/1/download/50x50?1585692000000')
+      jest.useRealTimers();
+    })
+  })
 })

+ 2 - 1
types/enums.ts

@@ -13,7 +13,8 @@ export const enum DENORMALIZER_TYPE {
 export const enum QUERY_TYPE {
   DEFAULT,
   MODEL,
-  ENUM
+  ENUM,
+  IMAGE
 }
 
 export const enum ABILITIES {

+ 11 - 1
types/interfaces.d.ts

@@ -25,6 +25,7 @@ interface ItemMenu {
   to?: string,
   children?: ItemsMenu,
   isExternalLink?: boolean,
+  actions?: ItemsMenu,
 }
 interface ItemsMenu extends Array<ItemMenu> {}
 
@@ -144,6 +145,7 @@ interface DataManager {
 interface UrlArgs {
   readonly type: QUERY_TYPE,
   readonly url?: string,
+  readonly baseUrl?: string,
   readonly enumType?: string,
   readonly model?: typeof Model,
   readonly rootModel?: typeof Model,
@@ -154,7 +156,15 @@ interface UrlArgs {
   readonly hook?: string
 }
 
-interface DataProviderArgs extends UrlArgs {}
+interface ImageArgs {
+  readonly id: number,
+  readonly height: number,
+  readonly width: number
+}
+
+interface DataProviderArgs extends UrlArgs {
+  imgArgs?: ImageArgs
+}
 interface DataPersisterArgs extends UrlArgs {
   data?: AnyJson
 }

+ 7 - 2
use/layout/Menus/accountMenu.ts

@@ -89,9 +89,14 @@ class AccountMenu extends BaseMenu implements Menu {
       children.push(this.constructMenu('print_my_licence', undefined, `/licence-cmf`, true))
     }
 
-    children.push(this.constructMenu('logout', undefined, `/logout`, true))
+    const accountMenu = this.constructMenu('my_account', 'fa-user', undefined, undefined, children, false)
 
-    return children.length > 0 ? this.constructMenu('my_account', 'fa-user', undefined, undefined, children) : null;
+    const actions: ItemsMenu = [];
+    actions.push(this.constructMenu('logout', undefined, `/logout`, true))
+
+    accountMenu.actions = actions
+
+    return accountMenu
   }
 }
 

+ 1 - 0
use/layout/Menus/baseMenu.ts

@@ -25,6 +25,7 @@ class BaseMenu {
    * @param {boolean} isOldLink est-ce un lien renvoyant vers l'ancien admin?
    * @param {Array<ItemMenu>} children Tableau d'ItemMenu représentant les sous menu du menu principal
    * @param {boolean} isExternalLink est-ce un lien renvoyant vers l'extérieur?
+   * @param {Array<ItemMenu>} actions Tableau d'ItemMenu représentant les actions devant apparaitre en bas du menu
    * @return {ItemMenu}
    */
   constructMenu (title: string, icon?: string, link?: string, isOldLink?: boolean, children?: Array<ItemMenu>, isExternalLink?: boolean): ItemMenu {

+ 6 - 1
use/layout/Menus/configurationMenu.ts

@@ -30,7 +30,8 @@ class ConfigurationMenu extends BaseMenu implements Menu {
     const children: ItemsMenu = []
 
     if (this.$ability.can('display', 'organization_page')) {
-      children.push(this.constructMenu('organization_page', undefined, `/main/organization/${this.$store.state.profile.organization.id}/dashboard`, true))
+      // children.push(this.constructMenu('organization_page', undefined, `/main/organization/${this.$store.state.profile.organization.id}/dashboard`, true))
+      children.push(this.constructMenu('organization_page', undefined, `/organization`))
     }
 
     if (this.$ability.can('display', 'cmf_licence_page')) {
@@ -77,6 +78,10 @@ class ConfigurationMenu extends BaseMenu implements Menu {
       children.push(this.constructMenu('course_duplication', undefined, '/duplicate_courses/', true))
     }
 
+    if (this.$ability.can('display', 'import_page')) {
+      children.push(this.constructMenu('import', undefined, '/import/all', true))
+    }
+
     if (children.length === 1) {
       return children[0]
     } else if (children.length > 0) {

File diff suppressed because it is too large
+ 302 - 282
yarn.lock


Some files were not shown because too many files changed in this diff