Procházet zdrojové kódy

licence export functionnal, sse ok

Olivier Massot před 2 roky
rodič
revize
e785d3dd99

+ 11 - 1
composables/data/useEntityFetch.ts

@@ -2,12 +2,17 @@ import {useEntityManager} from "~/composables/data/useEntityManager";
 import ApiResource from "~/models/ApiResource";
 import {AssociativeArray, Collection} from "~/types/data";
 import {AsyncData} from "#app";
+import {ComputedRef, Ref} from "vue";
 
 interface useEntityFetchReturnType {
     fetch: (model: typeof ApiResource, id: number) => AsyncData<ApiResource, ApiResource | true>,
     fetchCollection: (model: typeof ApiResource, parent?: ApiResource | null, query?: AssociativeArray) => AsyncData<Collection, any>
+    // @ts-ignore
+    getRef: <T extends ApiResource>(model: typeof T, id: Ref<number | null>) => ComputedRef<null | T>
 }
 
+
+// TODO: améliorer le typage des fonctions sur le modèle de getRef
 export const useEntityFetch = (lazy: boolean = false): useEntityFetchReturnType => {
     const { em } = useEntityManager()
 
@@ -23,6 +28,11 @@ export const useEntityFetch = (lazy: boolean = false): useEntityFetchReturnType
         { lazy }
     )
 
+    // @ts-ignore
+    const getRef = <T extends ApiResource>(model: typeof T, id: Ref<number | null>): ComputedRef<T | null> => {
+        return computed(() => (id.value ? em.find(model, id.value) as T : null))
+    }
+
     //@ts-ignore
-    return { fetch, fetchCollection }
+    return { fetch, fetchCollection, getRef }
 }

+ 2 - 3
composables/data/useEntityManager.ts

@@ -3,11 +3,10 @@ import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
 import ApiResource from "~/models/ApiResource";
 import {useRepo} from "pinia-orm";
 
-let entityManager:EntityManager|null = null
+let entityManager: EntityManager | null = null
 
 export const useEntityManager = () => {
-    //Avoid memory leak
-    if(entityManager === null){
+    if (entityManager === null) {
         const { apiRequestService } = useAp2iRequestService()
         const getRepo = (model: typeof ApiResource) => useRepo(model)
 

+ 0 - 14
composables/data/useFileManager.ts

@@ -1,14 +0,0 @@
-import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
-import FileManager from "~/services/data/fileManager";
-
-let fileManager: FileManager | null = null
-
-export const useFileManager = () => {
-    //Avoid memory leak
-    if (fileManager === null) {
-        const { apiRequestService } = useAp2iRequestService()
-        fileManager = new FileManager(apiRequestService)
-    }
-
-    return { fileManager }
-}

+ 19 - 0
composables/utils/useDownloadFile.ts

@@ -0,0 +1,19 @@
+import {useAp2iRequestService} from "~/composables/data/useAp2iRequestService";
+import File from "~/models/Core/File"
+import FileSaver from "file-saver";
+
+export const useDownloadFile = async (file: File) => {
+    const { apiRequestService } = useAp2iRequestService()
+
+    const downloadUrl = `api/download/${file.id}`
+
+    const response: any = await apiRequestService.get(downloadUrl)
+
+    if (!response || response.size === 0) {
+        console.error('Error: file ' + file.id + ' not found')
+    }
+
+    const blob = new Blob([response], { type: response.type })
+
+    FileSaver.saveAs(blob, file.name ?? 'unknown');
+}

+ 3 - 1
package.json

@@ -30,11 +30,13 @@
     "@nuxtjs/i18n": "^8.0.0-beta.9",
     "@pinia-orm/nuxt": "^1.1.7",
     "@pinia/nuxt": "0.4.6",
+    "@types/file-saver": "^2.0.5",
     "@types/js-yaml": "^4.0.5",
     "@types/vue-the-mask": "^0.11.1",
     "cleave.js": "^1.6.0",
     "date-fns": "^2.29.3",
     "event-source-polyfill": "^1.0.31",
+    "file-saver": "^2.0.5",
     "js-yaml": "^4.1.0",
     "libphonenumber-js": "1.10.19",
     "nuxt": "^3.2.0",
@@ -46,7 +48,7 @@
     "vite-plugin-vuetify": "^1.0.1",
     "vue-tel-input-vuetify": "^1.5.3",
     "vue-the-mask": "^0.11.1",
-    "vuetify": "3.1.5",
+    "vuetify": "3.1.6",
     "yaml-import": "^2.0.0"
   },
   "devDependencies": {

+ 44 - 35
pages/cmf_licence/organization.vue

@@ -14,7 +14,7 @@
     >
       <div class="ma-12">
         <v-btn
-            v-if="!filePending && file === null"
+            v-if="!pending && file === null"
             @click="submit"
         >
           {{ $t('generate') }}
@@ -23,8 +23,8 @@
         <v-btn
             v-else
             color="primary"
-            :loading="filePending"
-            :disabled="filePending"
+            :loading="pending"
+            :disabled="pending"
             target="_blank"
             @click="download"
         >
@@ -42,71 +42,80 @@ import {useEntityFetch} from "~/composables/data/useEntityFetch";
 import {useSseStore} from "~/stores/sse";
 import {useEntityManager} from "~/composables/data/useEntityManager";
 import LicenceCmfOrganizationER from "~/models/Export/LicenceCmfOrganizationER";
-import {useFileManager} from "~/composables/data/useFileManager";
+import {useDownloadFile} from "~/composables/utils/useDownloadFile";
+import {useRepo} from "pinia-orm";
 
 const { em } = useEntityManager()
-const { fetch } = useEntityFetch()
-const { fileManager } = useFileManager()
+const { getRef } = useEntityFetch()
 
 const sseStore = useSseStore()
 const async = () => { return sseStore.connected }
 
-const file: Ref<File | null> = ref(null)
+const fileId = ref(null)
 
-const submittingRequest: Ref<boolean> = ref(false)
+// TODO: il y a quelque chose à creuser ici, cette ref est nécessaire pour garder le lien avec le store,
+//       car on ne peut pas directement passer par le fetch de useEntityFetch ici (puisqu'on a pas encore l'id
+//       au chargement). Il faudrait voir si on ne pourrait pas intégrer ça au fetch, ou faire un fetchOrNull, qui
+//       combine fetch et getRef?
+const file: ComputedRef<File | null> = getRef(File, fileId)
 
-const filePending: ComputedRef<boolean> = computed(() => {
-  return submittingRequest.value || (file.value !== null && file.value.status === 'PENDING')
+// Submitting export request
+const submitting: Ref<boolean> = ref(false)
+
+// File is downloading
+const downloading: Ref<boolean> = ref(false)
+
+const pending: ComputedRef<boolean> = computed(() => {
+  return submitting.value || (file.value !== null && file.value.status === 'PENDING') || downloading.value
 })
 
 const submit = async () => {
-  submittingRequest.value = true
+  submitting.value = true
 
   const exportRequest = em.newInstance(LicenceCmfOrganizationER)
 
   exportRequest.format = 'pdf'
 
   if (async()) {
-    exportRequest.async = false
+    exportRequest.async = true
   } else {
-    console.error('SSE unavailable - File downloaded synchronously')
+    console.error('SSE unavailable - File will be downloaded synchronously')
   }
 
-  // Send the export request and get the receipt
-  const receipt = await em.persist(LicenceCmfOrganizationER, exportRequest)
-  if (receipt.fileId === null) {
-    throw new Error("Missing file's id, abort")
-  }
+  try {
+    // Send the export request and get the receipt
+    const receipt = await em.persist(LicenceCmfOrganizationER, exportRequest)
+    if (receipt.fileId === null) {
+      throw new Error("Missing file's id, abort")
+    }
+    fileId.value = receipt.fileId
 
-  file.value = await em.fetch(File, receipt.fileId as number, true) as File
+    // Fetch the newly created file from API. If export is async, it will be a record about a pending file,
+    // SSE will update its status to ready when it'll be.
+    await em.fetch(File, receipt.fileId)
 
-  submittingRequest.value = false
+  } finally {
+    submitting.value = false
+  }
 }
 
 const download = async () => {
+  downloading.value = true
+
   if (file.value === null) {
     console.error("File is not defined yet, impossible to download")
     return
   }
 
-  if (filePending.value) {
+  if (file.value.status === 'PENDING') {
     console.error("File is still pending, impossible to download")
     return
   }
 
-  const response = await fileManager.download(file.value.id as number)
-
-  const blob = new Blob([response], { type: response.type })
-
-  const url = window.URL.createObjectURL(blob)
-
-  const link = document.createElement('a')
-  link.href = url
-  link.download = file.value.name ?? 'unknown'
-  link.target = '_blank'
-  link.click()
-
-  link.remove()
-  window.URL.revokeObjectURL(url)
+  try {
+    await useDownloadFile(file.value)
+  } finally {
+    downloading.value = false
+  }
 }
 </script>

+ 11 - 3
plugins/sse.client.ts

@@ -1,6 +1,7 @@
 import SseSource from "~/services/sse/sseSource";
 import {useAccessProfileStore} from "~/stores/accessProfile";
 import {useSseStore} from "~/stores/sse";
+import {AnyJson} from "~/types/data";
 
 /**
  * Setup SSE EventSource, allowing the app to listen for Mercure updates
@@ -19,12 +20,19 @@ export default defineNuxtPlugin(nuxtApp => {
     const accessProfile = useAccessProfileStore()
     const sseStore = useSseStore()
 
+    const onOpen = () => sseStore.connected = true
+    const onClose = () => sseStore.connected = false
+
+    const onMessage = (eventData: AnyJson) => {
+        sseStore.addEvent(eventData as any)
+    }
+
     const sseSource = new SseSource(
         runtimeConfig.baseUrlMercure,
         "access/" + accessProfile.id,
-        () => { sseStore.connected = true },
-        (eventData) => { sseStore.addEvent(eventData as any) },
-        () => { sseStore.connected = false },
+        onOpen,
+        onMessage,
+        onClose,
     )
 
     sseSource.subscribe()

+ 3 - 1
services/data/entityManager.ts

@@ -14,6 +14,7 @@ import _ from "lodash"
 /**
  * Entity manager: make operations on the models defined with the Pinia-Orm library
  *
+ * TODO: améliorer le typage des méthodes sur le modèle de find()
  * @see https://pinia-orm.codedredd.de/
  */
 class EntityManager {
@@ -116,6 +117,7 @@ class EntityManager {
      *                  record is also updated.
      */
     public save(model: typeof ApiResource, instance: ApiResource, permanent: boolean = false): ApiResource {
+        instance = this.cast(model, instance)
         if (permanent) {
             this.saveInitialState(model, instance)
         }
@@ -124,7 +126,7 @@ class EntityManager {
 
     /**
      * Find the model instance in the store
-     * TODO: comment réagit la fonction si l'id n'existe pas?
+     * TODO: comment réagit la fonction si l'id n'existe pas dans le store?
      *
      * @param model
      * @param id

+ 0 - 24
services/data/fileManager.ts

@@ -1,24 +0,0 @@
-import ApiRequestService from "./apiRequestService";
-import File from "~/models/Core/File"
-
-class FileManager {
-    private apiRequestService: ApiRequestService;
-
-    public constructor(apiRequestService: ApiRequestService) {
-        this.apiRequestService = apiRequestService
-    }
-    public async download(id: number) {
-
-        const downloadUrl = `api/download/${id}`
-
-        const response: any = await this.apiRequestService.get(downloadUrl)
-
-        if(!response || response.size === 0) {
-            console.error('Error: file ' + id + ' not found or invalid')
-        }
-
-        return response
-    }
-}
-
-export default FileManager

+ 0 - 24
services/data/normalizer/fileNormalizer.ts

@@ -1,24 +0,0 @@
-/**
- * @category Services/serializer/normalizer
- * @class Default
- * Classe assurant la normalization par défaut
- */
-class FileNormalizer {
-  /**
-   * On transforme les data en FormData et on les renvois
-   * @return {any} réponse
-   * @param data
-   * @param file
-   */
-  public static normalize (data: any, file: string): any {
-    // TODO : intégrer au FileManager
-    const fileData = new FormData();
-    for(const key in data){
-      fileData.set(key, data[key])
-    }
-
-    fileData.set('file', file as string)
-    return fileData
-  }
-}
-export default FileNormalizer

+ 2 - 2
services/sse/sseSource.ts

@@ -7,7 +7,7 @@ class SseSource {
   protected readonly onClose: (() => void)
   protected readonly withCredentials: boolean
   protected eventSource: EventSourcePolyfill | null = null
-  constructor(
+  constructor (
     mercureHubBaseUrl: string,
     topic: string,
     onOpen: (() => void),
@@ -28,7 +28,7 @@ class SseSource {
       url,
       {
         withCredentials: withCredentials,
-        heartbeatTimeout: 45 * 1000 // in ms, timeout can not be disabled yet, so I set it very large instead
+        heartbeatTimeout: 45 * 1000 // in ms
       }
     );
   }

+ 14 - 4
yarn.lock

@@ -1321,6 +1321,11 @@
   resolved "https://registry.yarnpkg.com/@types/event-source-polyfill/-/event-source-polyfill-1.0.1.tgz#ffcb4ca17bc35bc1ca5d3e047fe833292bb73c32"
   integrity sha512-dls8b0lUgJ/miRApF0efboQ9QZnAm7ofTO6P1ILu8bRPxUFKDxVwFf8+TeuuErmNui6blpltyr7+eV72dbQXlQ==
 
+"@types/file-saver@^2.0.5":
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7"
+  integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==
+
 "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
@@ -3886,6 +3891,11 @@ file-entry-cache@^6.0.1:
   dependencies:
     flat-cache "^3.0.4"
 
+file-saver@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
+  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
 file-uri-to-path@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -8158,10 +8168,10 @@ vue@^3.2.47:
     "@vue/server-renderer" "3.2.47"
     "@vue/shared" "3.2.47"
 
-vuetify@3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.1.5.tgz#6cf56891f2fa7502feec1a7b50bef6aa4bc4623e"
-  integrity sha512-6DJYNuKrbxuL+MThmj+VbtvK9X3mY+s8Rqj1juj4RyeLSIAhE3byZf8Bo2/GxrU5Jv3zR07LZdpiq4wIl0ONzA==
+vuetify@3.1.6:
+  version "3.1.6"
+  resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.1.6.tgz#9ba513ac7b3ca9b0c7817dc63a8c18079eea9e49"
+  integrity sha512-4We27L5ksy8esNKfUPWPucEU+M7XJDO2o66zXL5qiBVbALwshcK2CAobLQMx+ALtkLENXAYNH3RzvfAYjTi3Aw==
 
 vuetify@^2.2.11:
   version "2.6.14"