Quellcode durchsuchen

finalize InputImage (except for the persisting part)

Olivier Massot vor 2 Jahren
Ursprung
Commit
10b588c60a

+ 4 - 0
components/Layout/Dialog.vue

@@ -84,6 +84,10 @@ const _show = computed(() => props.show) as boolean
     }
   }
 
+  .dialog-container {
+    overflow-x: scroll;
+  }
+
   .dialog-text-container {
     max-height: 70vh;
     overflow: auto;

+ 1 - 5
components/Layout/Parameters/General.vue

@@ -65,7 +65,7 @@
             <span class="mb-1">{{ $t('qrCode')}} </span>
             <UiInputImage
                 v-if="organizationProfile.isCMFCentralService"
-                v-model="parameters['qrCode']"
+                v-model="parameters.qrCode"
                 field="qrCode"
                 label="licenceQrCode"
                 :width="120"
@@ -95,7 +95,3 @@ const { data: parameters, pending } = fetch(Parameters, organizationProfile.para
 
 </script>
 
-<style scoped lang="scss">
-
-
-</style>

+ 9 - 3
components/Ui/Image.vue

@@ -44,7 +44,7 @@ const props = defineProps({
    * Id de l'image (null si aucune)
    */
   imageId: {
-    type: Number as PropType<Number | null>,
+    type: Number as PropType<number | null>,
     required: false,
     default: null
   },
@@ -80,7 +80,7 @@ const { fetch } = useImageFetch()
 
 const defaultImagePath = props.defaultImage ?? ImageManager.defaultImage
 
-const { data: imageSrc, pending, refresh } = fetch(props.imageId ?? null, defaultImagePath, props.height, props.width) as any
+const { data: imageSrc, pending, refresh: refreshImage } = fetch(props.imageId ?? null, defaultImagePath, props.height, props.width) as any
 
 
 const emit = defineEmits(['overlay-clicked'])
@@ -89,9 +89,15 @@ const emit = defineEmits(['overlay-clicked'])
  * Si l'id change, on recharge l'image
  */
 const unwatch: WatchStopHandle = watch(() => props.imageId, async () => {
-  await refresh()
+  await refreshImage()
 })
 
+const refresh = () => {
+  refreshImage()
+}
+
+defineExpose({ refresh })
+
 /**
  * Lorsqu'on démonte le component, on supprime le watcher
  */

+ 97 - 76
components/Ui/Input/Image.vue

@@ -6,6 +6,7 @@ Assistant de création d'image
 <template>
   <div class="input-image" >
     <UiImage
+        ref="imageElement"
         :image-id="modelValue"
         :default-image="defaultImage"
         :width="width"
@@ -59,6 +60,7 @@ Assistant de création d'image
                 {{$t('upload_image')}}
               </button>
             </div>
+            <span class="max-size-label">{{ $t('max_size_4_mb') }}</span>
           </div>
 
         </div>
@@ -86,6 +88,8 @@ import ApiResource from "~/models/ApiResource";
 import {useImageManager} from "~/composables/data/useImageManager";
 import { Cropper } from 'vue-advanced-cropper'
 import 'vue-advanced-cropper/dist/style.css';
+import {FILE_FOLDER, FILE_STATUS, FILE_TYPE, FILE_VISIBILITY, TYPE_ALERT} from "~/types/enum/enums";
+import {usePageStore} from "~/stores/page";
 
 const props = defineProps({
   /**
@@ -136,14 +140,16 @@ const props = defineProps({
 
 const { em } = useEntityManager()
 const { imageManager } = useImageManager()
+const pageStore = usePageStore()
 
-const emit = defineEmits(['update', 'reload', 'reset', 'close'])
+const emit = defineEmits(['update:modelValue', 'update:image', 'reset'])
 
 /**
  * Références à des composants
  */
 const fileInput: Ref<null | any> = ref(null)
 const cropper: Ref<any> = ref(null)
+const imageElement: Ref<any> = ref(null)
 
 /**
  * L'objet File contenant les informations de l'image
@@ -175,6 +181,8 @@ const uploadedImage: Ref<AnyJson> = ref({
   name: null
 })
 
+const MAX_FILE_SIZE = 4 * 1024 * 1024
+
 /**
  * Coordonnées du cropper
  */
@@ -202,82 +210,85 @@ const defaultSize = (params: any): { width: number, height: number } | null => {
   }
 }
 
-const openModal = () => {
-  if (!file.value) {
-    return
-  }
-
-  if (file.value.config) {
-    const config = JSON.parse(file.value.config)
-    coordinates.value.left = config.x
-    coordinates.value.top = config.y
-    coordinates.value.height = config.height
-    coordinates.value.width = config.width
-  }
-
-  uploadedImage.value.name = file.value.name
-  uploadedImage.value.id = file.value.id
-  uploadedImage.value.src = image.value
-
+/**
+ * Affiche la modale d'upload / modification de l'image
+ */
+const openModal = async () => {
   showModal.value = true
+  pending.value = true
 
-  console.log(defaultSize.value)
-}
-
+  if (props.modelValue !== null) {
+    // Un objet File existe déjà: on le récupère
+    file.value = await em.fetch(File, props.modelValue)
+    image.value = await imageManager.get(props.modelValue)
+
+    if (file.value.config) {
+      const config = JSON.parse(file.value.config)
+      coordinates.value.left = config.x
+      coordinates.value.top = config.y
+      coordinates.value.height = config.height
+      coordinates.value.width = config.width
+    }
 
+    uploadedImage.value.name = file.value.name
+    uploadedImage.value.id = file.value.id
+    uploadedImage.value.src = image.value
 
-/**
- * Charge le File et l'image correspondante.
- * Si le File possède une configuration, l'applique à l'image.
- *
- * @param id
- */
-const loadFile = async (id: number | string) => {
-  pending.value = true
-  file.value = await em.fetch(File, id as number)
-  image.value = await imageManager.get(id as number)
+  } else {
+    // Nouveau File
+    file.value = em.newInstance(File)
+  }
 
   pending.value = false
 }
 
-if (props.modelValue !== null) {
-  loadFile(props.modelValue)
+/**
+ * Réinitialise l'image sélectionnée
+ */
+const reset = () => {
+  uploadedImage.value.src = null
+  uploadedImage.value.file = null
+  uploadedImage.value.name = null
+  uploadedImage.value.id = null
+  URL.revokeObjectURL(uploadedImage.value.src)
 }
 
 /**
  * Upload une image depuis le poste client
  * @param event
  */
-const uploadImage = (event:any) => {
+const uploadImage = (event: any) => {
   const { files } = event.target
 
   if (!files || !files[0]) {
     return
   }
 
+  const uploadedFile = files[0]
+
+  if (uploadedFile.size > MAX_FILE_SIZE) {
+    pageStore.alerts.push({type: TYPE_ALERT.ALERT, messages: ['file_too_large'] })
+    return
+  }
+
   reset()
+
   uploadedImage.value.name = files[0].name
   uploadedImage.value.src = URL.createObjectURL(files[0])
   uploadedImage.value.file = files[0]
-}
 
-/**
- * Lorsque le cropper change de position / taille, on met à jour les coordonnées
- * @param config
- */
-const onCropperChange = ({ coordinates: config } : any) => {
-  coordinates.value = config;
+  coordinates.value.top = 0
+  coordinates.value.left = 0
+  coordinates.value.height = uploadedImage.value.height
+  coordinates.value.width = uploadedImage.value.width
 }
 
 /**
- * Réinitialise l'image sélectionnée
+ * Lorsque le cropper change de position / taille, on met à jour les coordonnées
+ * @param newCoordinates
  */
-const reset = () => {
-  uploadedImage.value.src = null
-  uploadedImage.value.file = null
-  uploadedImage.value.name = null
-  uploadedImage.value.id = null
-  URL.revokeObjectURL(uploadedImage.value.src)
+const onCropperChange = ({ coordinates: newCoordinates } : any) => {
+  coordinates.value = newCoordinates;
 }
 
 /**
@@ -286,7 +297,18 @@ const reset = () => {
 const cancel = () => {
   reset()
   showModal.value = false
-  emit('close')
+}
+
+/**
+ * Construit la nouvelle config du fichier à partir des réglages actuels
+ */
+const updateFileConfig = () => {
+  file.value!!.config = JSON.stringify({
+    x: coordinates.value.left,
+    y: coordinates.value.top,
+    height: coordinates.value.height,
+    width: coordinates.value.width
+  })
 }
 
 /**
@@ -300,19 +322,12 @@ const saveNewImage = async () => {
   // On créé l'objet File à sauvegarder
   file.value.name = uploadedImage.value.name
   file.value.imgFieldName = props.field
+  file.value.visibility = FILE_VISIBILITY.EVERYBODY
+  file.value.folder = FILE_FOLDER.IMAGES
+  file.value.status = FILE_STATUS.READY
+  file.value.type = FILE_TYPE.UPLOADED
 
-  // Construit la nouvelle config du fichier
-  file.value!!.config = JSON.stringify({
-    x: coordinates.value.left,
-    y: coordinates.value.top,
-    height: coordinates.value.height,
-    width: coordinates.value.width
-  })
-
-  // TODO: utiliser des enums?
-  file.value.visibility = 'EVERYBODY'
-  file.value.folder = 'IMAGES'
-  file.value.status = 'READY'
+  updateFileConfig()
 
   if (props.ownerId) {
     // TODO: revoir
@@ -321,9 +336,10 @@ const saveNewImage = async () => {
 
   // TODO: A revoir, on doit pouvoir persister l'image aussi
   const returnedFile = await em.persist(File, file.value)
+  // await imageManager.persist(file.value, uploadedImage.src)
 
   //On émet un évent afin de mettre à jour le formulaire de départ
-  emit('update', returnedFile.data['@id'])
+  emit('update:modelValue', returnedFile.id)
 }
 
 /**
@@ -334,20 +350,9 @@ const saveExistingImage = async () => {
     throw new Error('No File object defined')
   }
 
-  file.value.id = uploadedImage.value.id
-
-  // Construit la nouvelle config du fichier
-  file.value!!.config = JSON.stringify({
-    x: coordinates.value.left,
-    y: coordinates.value.top,
-    height: coordinates.value.height,
-    width: coordinates.value.width
-  })
+  updateFileConfig()
 
   await em.persist(File, file.value) // TODO: à revoir
-
-  // On émet un évent afin de mettre à jour le formulaire de départ
-  emit('reload')
 }
 
 /**
@@ -358,6 +363,7 @@ const save = async () => {
   if (!file.value) {
     throw new Error('No File object defined')
   }
+  pageStore.loading = true
 
   if (uploadedImage.value.src && uploadedImage.value.src !== image.value) {
     // Une nouvelle image a été uploadée
@@ -367,8 +373,13 @@ const save = async () => {
     await saveExistingImage()
   } else {
     // On a reset l'image
-    emit('reset')
+    emit('update:modelValue', null)
   }
+
+  imageElement.value.refresh()
+
+  showModal.value = false
+  pageStore.loading = false
 }
 
 /**
@@ -382,10 +393,11 @@ onUnmounted(() => {
 </script>
 
 <style scoped lang="scss">
-  .vue-advanced-cropper__stretcher{
+  :deep(.vue-advanced-cropper__stretcher) {
     height: auto !important;
     width: auto !important;
   }
+
   .loading{
     height: 300px;
   }
@@ -442,4 +454,13 @@ onUnmounted(() => {
       }
     }
   }
+
+  .max-size-label {
+    display: block;
+    width: 100%;
+    text-align: center;
+    font-size: 13px;
+    color: rgb(var(--v-theme-on-neutral-soft));
+    margin-top: 6px;
+  }
 </style>

+ 4 - 2
lang/fr.json

@@ -301,7 +301,7 @@
   "phoneNumberInvalid": "Numéro de téléphone invalide",
   "logo": "Logo",
   "subdomain": "Sous-domaine",
-  "upload_image": "Sélectionner une image",
+  "upload_image": "Charger une image",
   "of": "de",
   "allResult": "Tous",
   "itemsPerPage": "Nombre de résultats par page",
@@ -658,5 +658,7 @@
   "create_a_new_residence_area": "Créer une nouvelle zone de résidence",
   "residence_areas_breadcrumbs": "Zones de résidence",
   "super_admin_explanation_text": "Le compte super-admin possède tous les droits de gestion sur votre logiciel. On l’utilise entre autre pour la gestion de votre site internet, pour créer les comptes des membres de votre structure à la première connexion au logiciel, ou dans des situations de dépannage.",
-  "cycles_breadcrumbs": "Enseignements"
+  "cycles_breadcrumbs": "Enseignements",
+  "max_size_4_mb": "Taille maximum: 4 MO",
+  "file_too_large": "Le fichier est trop volumineux"
 }

+ 1 - 1
models/Organization/Parameters.ts

@@ -106,7 +106,7 @@ export default class Parameters extends ApiModel {
 
   @Str(null)
   @IriEncoded(File)
-  declare qrCode: string | null
+  declare qrCode: number | null
 
   @Str('Europe/Paris')
   declare timezone: string | null

+ 24 - 0
types/enum/enums.ts

@@ -75,3 +75,27 @@ export const enum SUBMIT_TYPE {
   SAVE_AND_BACK = 'save_and_back'
 }
 
+export const enum FILE_VISIBILITY {
+  EVERYBODY = 'EVERYBODY',
+  NOBODY = 'NOBODY',
+  ONLY_ORGANIZATION = 'ONLY_ORGANIZATION'
+}
+
+export const enum FILE_FOLDER {
+  IMAGES = 'IMAGES'
+}
+
+export const enum FILE_STATUS {
+  PENDING = 'PENDING',
+  READY = 'READY',
+  DELETED = 'DELETED',
+  ERROR = 'ERROR',
+}
+
+export const enum FILE_TYPE {
+  UNKNOWN = 'UNKNOWN',
+  NONE = 'NONE',
+  LICENCE_CMF ='LICENCE_CMF',
+  BILL ='BILL',
+  UPLOADED = 'UPLOADED'
+}