# Analyse - 09/2022 ## Workflow du traitement de données des adresses postales sur la fiche de la structure Etude du workflow des pages `pages/organization/index.vue` et `pages/organization/address/*` #### Get La page intègre le component `UiCollection`, qui prend en paramètres : * la query (récupérée depuis le repo) * le modèle de l'entity concernée * le modèle d'origine (ici Organization) * l'id d'origine (celui de l'orga) L'`UiCollection` récupère la méthode `getCollection` de `useDataUtils` (à qui elle a passé le `dataProvider`) Elle appelle ensuite cette méthode et récupère le `fetchState` et les items sous forme de ref Enfin, elle passe les items à son slot, qui les utilise pour afficher les données #### Create ##### Page `entity/new.vue` Passe le `$dataProvider` à `useDataUtils` qui retourne la méthode `createItem` Passe le store et le type d'entité à la méthode `createItem` qui retourne une fonction `create` et deux refs : `loading` (bool) et `item` (Model). La méthode `createItem` commit aussi le type du formulaire (`FORM_STATUS.CREATE`), et met le loading à true Côté client seulement: instancie une nouvelle Entité, et la passe à la méthode `create` qui la persist dans le repository La page inclut ensuite un component FormXxxx correspondant au type d'entité. ##### Component FormXxxx Peut utiliser des composables pour retourner des données complémentaires (ex: listes déroulantes). Il récupère alors le dataProvider depuis le context pour le passer aux méthodes du composable. Récupère le repo propre à l'entité via `repositoryHelper.getRepository(Xxxxx)` Génère une query depuis le repo, déclare éventuellement les relations à charger au passage Génère une méthode `submitActions` qui retourne une liste d'actions caractérisées par : type d'action (cf. `SUBMIT_TYPE`) et adresse à laquelle se rendre après l'action Inclut le composant générique UiForm et lui passe l'`id`, le `model`, la `query`, les `submitActions` ##### Component UiForm Importe via des methodes de composables : * `$dataPersister` et `store` * `router` * `markAsDirty`, `markAsNotDirty`, `readonly`, `nextStepFactory` Créé un store de type `Page` Créé une ref null nommée `form` Construit une computed ref `entry` qui sera passée au slot (le FormXxxx). Cette ref doit récupérer l'entrée du repo via `queryHelper.getFlattenEntry(query.value, id.value)` Construit la méthode `updateRepository` qui sera aussi passée au slot. Cette méthode invoque la méthode `markAsDirty()` qui met à jour le store Form pour le déclarer en dirty, maj la valeur du champs de l'entité correspondante et persist le tout dans le repo. Créé une méthode `validate` qui utilise la méthode du même nom du v-form (natif de vuetify) Créé une méthode `submit` qui reçoit un argument `next` (type de l'action exécutée): * Appelle la méthode `validate` * Si le formulaire est valide : * Appelles la méthode `$dataPersister.invoke` et récupère la réponse * ajoutes une alerte de type Success au store de la page * appelles la méthode nextStep en lui passant l'argument `next`, qui est donc le type de l'action exécutée * Sinon : * Ajoutes des alertes au store de la page et à un nouveau store de type Form (pqoi?) Créé la méthode `nextStep`, qui appelles la méthode `nextStepFactory` de `useForm`. Cette méthode associe les types d'actions à des callbacks appellant des méthodes de `useForm` (`save`, `sageAndGoto`). Exécute ensuite le callback retourné. Ces deux callbacks ne font qu'appeller le router pour rediriger vers la page suivante. Créé des méthodes `showDialog` et `closeDialog` qui mettent à jour le store générique Créé enfin une méthode `saveAndQuit` qui : * appelle la méthode `submit` * devrait appeller la méthode `quitForm` (voir plus bas), mais sauf erreur, le router a déjà été appellé par la méthode `submit`... Lorsqu'on quitte le formulaire, la boite de dialogue s'affiche et propose trois options : `back_to_form`, `save_and_quit`, `quit_form` Un clic sur le bouton `back_to_form` du formulaire appelle la méthode `closeDialog` qui ferme la boite de dialogue. Un clic sur le bouton `save_and_quit` du formulaire appelle la méthode `saveAndQuit` (voir au dessus) Un clic sur le bouton `quit_form` du formulaire appelle la méthode `quitForm`, qui à son tour: * appelles la méthode markAsNotDirty() * confirme dans le store la volonté de quitter * remet le store dans son état original ##### Component FormXxxx (à nouveau) Le formulaire reçoit de UiForm la ref `entry` et la méthode `updateRepository` L'entry correspond à l'entité dans le store telle que récupérée via le repo `updateRepository` sera appellée par chaque input qui sont mis à jour #### Edit ##### Page `entity/_id.vue` Idem *create*, excepté que le formulaire est inclus dans une page `entity/_id.vue` Passe le `$dataProvider` à `useDataUtils` qui retourne la méthode `getItemToEdit` Parse l'id de l'entité à éditer directement depuis l'url Passe l'id et le type d'entité à la méthode `getItemToEdit` qui appelle la méthode `$dataProvider.invoke` et retourne une ref `fetchState`. ## Autres Usages ### Entités ##### Via UiCollection GET > Exemple : l'affichage des adresses sur la fiche de la structure La page intègre le component `UiCollection`, qui prend en paramètres : * la query (récupérée depuis le repo) * le modèle de l'entity concernée * le modèle d'origine (ici Organization) * l'id d'origine (celui de l'orga) L'`UiCollection` récupère la méthode `getCollection` de `useDataUtils` (à qui elle a passé le `dataProvider`) Elle appelle ensuite cette méthode et récupère le `fetchState` et les items sous forme de ref Enfin, elle passe les items à son slot, qui les utilise pour afficher les données ##### Via UiItemFromUri GET > Exemple : l'affichage du pays dans le détail des adresses de la fiche de la structure Extrait l'id de l'Uri utilise le `useFetch` natif de nuxt en lui passant en callback un appel à `$dataProvider.invoke` L'item est ensuite retourné sous forme d'une ComputedRef qui retourne elle-même `queryHelper.getItem` ##### Via UiDataTable GET Prend en paramètres : * la query (récupérée depuis le repo) * le modèle de l'entity concernée * le modèle depuis lequel il est modifié * l'id du modèle depuis lequel il est modifié Utilise `useFetch` en lui passant en callback un appel à `$dataProvider.invoke(type: QUERY_TYPE.MODEL, ...)` ##### UiLayoutAlertBarCotisation GET Récupère l'état des cotisations avec un `useFetch` auquel on passe en callback un appel à `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` ##### Composable useCountryProvider GET Utilise `useFetch` en lui passant en callback un appel à `$dataProvider.invoke(type: QUERY_TYPE.MODEL, ...)` ##### Composable useTypeOfPracticeProvider GET Utilise `useFetch` en lui passant en callback un appel à `$dataProvider.invoke(type: QUERY_TYPE.MODEL, ...)` ##### Composable useMyProfile GET / EDIT Propose une méthode pour maj le profile via `$dataPersister.invoke({type: QUERY_TYPE.MODEL, ...})` #### Delete ##### Via UiButtonDelete DELETE Appelle directement `$dataDeleter.invoke` #### Cas particulier : UiLayoutHeaderNotification GET / EDIT Récupère les notifs depuis l'api via un useFetch avec un callback à `await $dataProvider.invoke({type: QUERY_TYPE.ENUM, enumType})` Récupère ensuite les notifs depuis le store sous forme de computed ref ### Enums ##### Via UiInputEnum GET Récupère l'enum via un useFetch avec un callback à `await $dataProvider.invoke({type: QUERY_TYPE.ENUM, enumType})` Lorsque les notifications ont été lues, créé un nouvel objet NotificationUser en : * mettant à jour le store avec `repositoryHelper.persist` * l'api avec `$dataPersister.invoke(type: QUERY_TYPE.MODEL, ...)` * ### Images ##### Via UiImage GET Récupère l'image grâce à la méthode `getOne` du composable `useImageProvider` (à qui on a passé le data provider et la config du context) `getOne` appelle un dataProvider avec le type `QUERY_TYPE.IMAGE` et lui passe les arguments de config de l'image (id, hauteur, largeur) En cas d'erreur, retourne une image par défaut. ##### Via UiInputImage GET Si un id a été passé aux props, récupère l'objet File via un useFetch avec un callback à `await $dataProvider.invoke({type: QUERY_TYPE.DEFAULT, url: 'api/files', id: props.existingImageId })` On récupère ensuite l'image grâce à la méthode `getOne` du composable `useImageProvider` (à qui on a passé le data provider et la config du context) `getOne` appelle un dataProvider avec le type `QUERY_TYPE.IMAGE` et lui passe les arguments de config de l'image (id, hauteur, largeur) En cas d'erreur, retourne une image par défaut. ##### Via UiInputImage GET / PUT Met à jour le store avec `repositoryHelper.persist` Envoie la requête de maj du File avec `$dataPersister.invoke(type: QUERY_TYPE.MODEL, ...)` POST Créé un nouvel objet File à sauvegarder L'envoie à l'API avec `$dataPersister.invoke(type: QUERY_TYPE.FILE, ...)` ### Api externes ou routes custom #### UiInputAutocompleteWithApi Appelle `useFetch` en lui passant le callback `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` #### UiTemplateMobytStatus Appelle `useFetch` en lui passant le callback `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` #### UiMap Appelle la route `/api/gps-coordinate-searching` via un appel à `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` #### Composable useAccessesProvider Appelle la route `/api/access_people` via un appel à `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` #### Composable useAddressPostalUtils Appelle la route `https://api-adresse.data.gouv.fr/search/...` via un appel à `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` #### Composable useValidator Appelle la route `/api/siret-checking` via un appel à `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` ou Appelle la route `/api/subdomains` via un appel à `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` #### Page subscription Appelle la route `/api/dolibarr/account/` via un appel à `$dataProvider.invoke({type: QUERY_TYPE.DEFAULT, ...})` ## Propositions ### Notes diverses ##### Stores x faire un store 'appState', renommer form en formState? renommer page en pageState? >> oui, renommer les stores form, page, ... en formStore, pageStore, ... x utiliser des interfaces pour le state des stores : https://pinia.vuejs.org/core-concepts/state.html#typescript (déja fait a priori) >> déjà fait x j'ai l'impression que les dialogs ne sont pas gérés depuis le store "page", ce serait ptêt pas mal? >> NA ##### Pinia x utiliser les hooks pinia pour répercuter les maj sur l'api? >> https://pinia-orm.codedredd.de/guide/model/lifecycle-hooks >> créer nos propres hooks, mais s'inspirer des hooks pinia-orm ##### Data managers x je pense qu'on devrait sortir le `$nuxt.$loading.start()` du service baseDataManager, c'est pas trop à sa place ici >> plus d'actualité dans nuxt 3 x le data deleter ne passe pas par un dataProcessor pour mettre à jour le store. mais est-ce nécessaire au final? >> plus d'actualité dans nuxt 3 x est-ce qu'on devrait pas choisir soit de récupérer les données dans des composables (ex: useTypeOfPracticeProvider, useCountryProvider), soit dans les components? si on maintient les composables, on pourrait les mettre dans un sous-dossier ? ou même factoriser la méthode ? >> plus d'actualité x DataPersister L25 : c'est bizarre de stocker l'objet sérialisé dans une de ses propres props non ? on devrait peut-être créer une nouvelle variable et laisser là l'objet DataPersisterArgs x ce serait pas mal de passer juste les props nécessaire aux différentes méthodes plutôt que le paquet `queryArgs` à chaque fois, je pense qu'on y verrait plus clair sur qui fait quoi x Les services data managers devraient être indépendant de VuexOrm/PiniaOrm, on devrait donc remplacer le param 'query' du data persister par des données sérialisables ##### Forms x passer le create et le loader de la page Address/new dans le formulaire? faire la même chose partout? x`const entryCopy = query.value.first()` dabs `useForm` (L185): on pourrait peut-être factoriser ça dans un composable? au passage, v-form a une méthode `reset`, ça pourrait ptêt faire le job? x UiForm, methode `saveAndQuit`: est-ce qu'on passe dans la méthode `quitForm` au final? puisque la méthode nextStep appellée depuis submit a déjà dû faire la redirection? x page address/_id, ligne 18 : on ne pourrait pas faire mieux pour le `const id = parseInt(route.value.params.id)`? faire ça en amont? dans un service? ou carrément parser les paramètres directement depuis un middleware et les stocker dans un store dédié en lecture seule? x plutôt que d'instancier une entité dans la page et de la passer au form, est-ce qu'on ne passerait pas un param 'create' au form? il faudrait lever des erreurs si `create == true & id != null` ou `create == false & id == null` x comment imposer des propriétés à tous les FormXxxx? Serait-il possible de mettre en place une interface pour l'array props, et de la tester depuis UiForm? x useError, ligne 7 : je suppose que la docstring est pas la bonne? x uiForm, L185-188 : je passerais ça dans une méthode `resetEntry` par ex x components/Form/Organization/ContactPoint.vue, L62 : est-ce que l'action Back serait pas à sa place avec les actions SUBMIT_TYPE ? ##### Actions des forms x `actions[SUBMIT_TYPE.SAVE] = { path: `/organization/address/` }` : un moyen de rendre plus explicite que l'url dans path est la page où l'on se rend après l'action? x est-ce que les chemins où se rendre après les actions submit ne devraient pas être paramétrables dans les props du formulaire lui-même? x plutôt que de passer un param 'accordion' (`{ path: `/organization`, query: { accordion: 'address_postal' } }`), est-ce qu'on ne pourrait pas utiliser des anchors? ##### Divers x voir à sortir les services profiles/* et rights/* et à les passer dans les composables (il faudra maj les plugins ability et castl) x UiItemFromUri, ligne 52 : pqoi ne pas appeller directement `ModelsUtils.extractIdFromUri(uri)` x UiInputImage, ligne 112 : est-ce que ce ne serait pas à sa place dans un processor attaché au data provider QUERY_TYPE.IMAGE ça? x pourquoi avoir séparé les pages parameters et organization en deux? devrais-je faire la même chose avec subscription? (ex: pages/organization.vue et pages/parameters/index.vue) * component useDataUtils : * on pourrait renommer `getItemToEdit` en `getItem`? * on pourrait ptêt renommer la méthode `createItem` en `getItemCreator` ou un truc du genre, pour expliciter le fait qu'on retourne une méthode, pas qu'on créé directement l'objet x page communication : passer getCurrentWebsite dans un service? (s'il n'existe pas déjà) x UiCollection : on pourrait pas passer l'entry root plutot que son model+id? est-ce que le terme de parent serait plus précis ou pas ? x middleware/auth.ts : on devrait mettre l'uri du front "admin" non? * pour gérer les cas comme sur la page subdomains/id_, on pourrait sortir la logique de soumission et validation de UiForm et mettre ça dans un component séparé ou dans un composable? de cette façon, on pourrait utiliser cette logique sans être dans un formulaire classique (boutons, dialog…) * détail : dans UiCollection, on pourrait ptêt éviter des confusions en renommant la prop `newLink` en qqchose comme `pageNewUri`, ou `linkToNewPage`, ou même `linkToNew`, mais bon, c'est ptêt pas nécessaire... * useValidator : est-ce qu'on ne passerait pas le contenu de ces fonctions dans des services de validation? même chose pour les autres composable qui font des requêtes à l'api ou des calculs testables ### Structure #### Entity Manager Mettre en place un "guichet unique" pour les entités, nommé par ex entityManager Décliner les data-managers en : * EntityManager (`em`) * EnumManager (`enumManager`) * FileManager (`fileManager`) * ImageManager (`imageManager`), utile? ou fusionner avec le FileManager? Ces managers auraient des méthodes comme : `getRepository` (voir si nécessaire de garder l'étape des repos), `fetch`, `persist`, `delete`, `new` ou `init`, `flush` (?), `rollback` ou `reset` Ils seraient responsables de la validation, de l'état de loading, de l'état dirty Ils seraient importables depuis un composable du type `useDataManager` ou `useEntityManager` > Incorporer les méthodes de useDataUtils dans ce manager > Voir si utile de récupérer des méthodes de useForm? Pas sûr, mais l'objectif serait que useForm soit seul à interagir avec le store formState, et aussi le seul à interagir avec. L'idée serait que ce que l'on écrit comme ça aujourd'hui : const {getItemToEdit} = useDataUtils($dataProvider) const {fetchState} = getItemToEdit(id, Parameters) const repository: VuexRepository = repositoryHelper.getRepository(Parameters) const query: ComputedRef = computed(() => repository.query()) const entry: ComputedRef = computed(() => { return queryHelper.getFlattenEntry(query.value, id) }) Puisse s'écrire plus ou moins comme ça : const { em } = useEntityManager() { fetchState, entity } = em.fetch(Parameters, id) // où fetchState et entity serait des 'ref'? Ou même quelque chose comme ça ? const { em } = useEntityManager() let fetchState = ref(true) let parameters = ref(null) em.fetch(Parameters, id).onSuccess((e) => { fetchState.value = false; entity.value = e; }) Si on décide finalement de conserver la mécanique actuelle, je propose alors de revoir les termes employés, afin de bien identifier les différentes opérations #### Application aux différents types d'usage 1. Récupération et mise à jour du profil 2. Affichage des données d'une entité 3. Affichage d'une collection d'entités 4. Formulaire de création / édition d'une entité 5. Population d'un input par un enum 6. Population d'un input par des entités (ex: liste des pays) 7. Population d'un input depuis une source externe (ex: adresses) 8. Affichage / upload d'une image 9. Téléchargement / upload de fichier 10. Affichage et éditions des notifications 11. Validation de données (ex: siret) 12. Affichage de données issues d'une api externe ou de routes custom (ex: dolibarr, mobyt) ##### 1- Récupération et mise à jour du profil Actuellement, cette mise à jour se fait dans store/index.js : async nuxtServerInit ({ dispatch }, { req }) { await dispatch('initCookies', { req }) await dispatch('getUserProfile') }, async getUserProfile ({ dispatch }) { const myProfile = await this.app.context.$dataProvider.invoke({ type: QUERY_TYPE.DEFAULT, url: '/api/my_profile' }) await dispatch('profile/access/setProfile', myProfile.data) } Ici a priori, un simple appel à `Connection.get` pourrait suffire (il faut juste ajouter une méthode get pour les url custom, parce que getItem est dédié aux entities). D'autant que le denormalizer des data provider est le denormalizer Hydra, ce qui n'est **pas adapté** aux routes comme api/my_profile par ex. Ça donnerait un truc du genre : async getUserProfile ({ dispatch }) { const myProfile = await this.app.context.$connection.get('/api/my_profile') await dispatch('profile/access/setProfile', myProfile) } ##### 2- Affichage des données d'une entité Exemple de pages/organization.vue : const {store, $dataProvider} = useContext() const {getItemToEdit} = useDataUtils($dataProvider) const id = store.state.profile.organization.id const {fetchState} = getItemToEdit(id, Organization) const repository = repositoryHelper.getRepository(Organization) const query: ComputedRef = computed(() => repository.query()) const entry: ComputedRef = computed(() => { return queryHelper.getItem(query.value, id) }) return { entry, fetchState } Est-ce qu'on peut espérer quelque chose de ce genre ? const { em } = useEntityManager() const { fetchState, organization } = em.fetch(Organization, id) return { organization, fetchState } ##### 3- Affichage d'une collection d'entités Exemple de pages/organization/index.vue : Dans components/Ui/Collection.vue : const { rootModel, rootId, model, query }: ToRefs = toRefs(props) const { $dataProvider } = useContext() const { getCollection } = useDataUtils($dataProvider) const {fetchState} = getCollection(model.value, rootModel.value, rootId.value) const items: ComputedRef = computed(() => queryHelper.getCollection(query.value)) return { items, fetchState } Est-ce qu'on peut espérer quelque chose de ce genre ? const { em } = useEntityManager() const { fetchState, contactPoints } = em.fetchBy(ContactPoint, Organization, organization_id) return { contactPoints, fetchState } ##### 4- Formulaire de création / édition d'une entité Exemple de `pages/organization/contact_points/_id.vue` : Dans components/Form/Organization/ContactPoint.vue : Dans components/Ui/Form.vue : Ce qui pourrait peut-être donner quelque chose comme : pages/organization/contact_points/_id.vue : components/Form/Organization/ContactPoint.vue : Dans components/Ui/Form.vue : ##### 5- Population d'un input par un enum Exemple sur la page components/Form/Organization/ContactPoint.vue : Dans components/Ui/Input/Enum.vue : const { enumType } = props const { $dataProvider, store } = useContext() (...) const items: Ref> = ref([]) useFetch(async () => { items.value = await $dataProvider.invoke({ type: QUERY_TYPE.ENUM, enumType }) }) return { items, (...) } Ce qui pourrait peut-être donner quelque chose comme : components/Ui/Input/Enum.vue : const { enumType } = props const { enumManager } = useEnumManager() (...) const items: Ref> = enumManager.fetchByType(enumType) return { items, (...) } ##### 6- Population d'un input par des entités (ex: liste des pays) Exemple sur la page components/Form/Organization/Address.vue : Dans composables/data/useCountryProvider.ts : export function useCountryProvider($dataprovider: DataProvider){ const {fetch, fetchState} = useFetch(async () => { await $dataprovider.invoke({ type: QUERY_TYPE.MODEL, model: Country }) }) const countries = computed(() => { return repositoryHelper.findCollectionFromModel(Country) }) return { countries, fetch, fetchState } } Dans services/store/repository.ts : 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() } Ce qui pourrait peut-être donner quelque chose comme : page components/Form/Organization/Address.vue : ##### 7- Population d'un input depuis une source externe (ex : adresses) Exemple sur la page components/Form/Organization/Address.vue : (...) (...) (...) Dans components/Ui/Input/AutocompleteWithAPI.vue : useFetch(async () => { isLoading.value = true const r = await $dataProvider.invoke({ type: QUERY_TYPE.DEFAULT, url: props.remoteUrl, listArgs: { filters:[ {key: 'id', value: ids.join(',')} ] } }) isLoading.value = false remoteData.value = r.data items.value = r.data }) Dans composables/data/useAddressPostalUtils.ts : async function searchFunction(research: string, field: string): Promise> { if (research) { const response = await $dataProvider.invoke({ type: QUERY_TYPE.DEFAULT, url: `https://api-adresse.data.gouv.fr/search/?q=${research}&type=municipality&autocomplete=1&limit=20`, params: { noXaccessId: true } }) const apiResponse = response.data.features.map((data: AnyJson) => data.properties) (...) } Alternative : composables/data/useAddressPostalUtils.ts async function searchFunction(research: string, field: string): Promise> { if (research) { const response = await Connection.get({ url: `https://api-adresse.data.gouv.fr/search/?q=${research}&type=municipality&autocomplete=1&limit=20`, params: { noXaccessId: true } }) const apiResponse = response.features.map((data: AnyJson) => data.properties) (...) } ##### 8- Affichage / upload d'une image Sur la page pages/organization/index.vue : Dans components/Ui/Image.vue : Dans components/Ui/Input/Image.vue : const {$dataProvider, $config, $dataPersister} = useContext() const {getOne} = useImageProvider($dataProvider, $config) const fileToSave = new File() const cropper:Ref = ref(null) const image: Ref = ref({ id: null, src: null, file: null, name: null }) (...) if(props.existingImageId){ useFetch(async () => { const result: ApiResponse = await $dataProvider.invoke({ type: QUERY_TYPE.DEFAULT, url: 'api/files', id: props.existingImageId }) const config = JSON.parse(result.data.config) (...) }) } const { fetchState, imageLoaded } = getOne(props.existingImageId) const unwatch: WatchStopHandle = watch(imageLoaded, (newValue, oldValue) => { if (newValue === oldValue || typeof newValue === 'undefined') { return } image.value.src = newValue }) const save = async () => { fileToSave.config = JSON.stringify({ x: coordinates.value.left, y: coordinates.value.top, height: coordinates.value.height, width: coordinates.value.width }) //Cas d'un PUT : l'image existe déjà on bouge simplement le cropper if(image.value.id){ fileToSave.id = image.value.id repositoryHelper.persist(File, fileToSave) await $dataPersister.invoke({ type: QUERY_TYPE.MODEL, model: File, id: fileToSave.id }) //On émet un évent afin de mettre à jour le formulaire de départ emit('reload') } //Post : on créer une nouvelle image donc on passe par l'api legacy... else{ if(image.value.file){ //On créer l'objet File à sauvegarder fileToSave.name = image.value.name fileToSave.imgFieldName = props.field fileToSave.visibility = 'EVERYBODY' fileToSave.folder = 'IMAGES' if(props.ownerId) fileToSave.ownerId = props.ownerId //Appel au datapersister const response: ApiResponse = await $dataPersister.invoke({ type: QUERY_TYPE.FILE, baseUrl: $config.baseURL_Legacy, data: fileToSave.$toJson(), file: image.value.file }) //On émet un évent afin de mettre à jour le formulaire de départ emit('update', response.data['@id']) }else{ //On reset l'image : on a appuyer sur "poubelle" puis on enregistre emit('reset') } } } Dans composables/data/useImageProvider.ts : async function provideImg(id: number, height: number = 0, width: number = 0) { return await $dataProvider.invoke({ type: QUERY_TYPE.IMAGE, baseUrl: $config.baseURL_Legacy, imgArgs: { id: id, height: height, width: width } }) } Alternative : pages/organization/index.vue : components/Ui/Image.vue : components/Ui/Input/Image.vue : const { em } = useEntityManager() const { imageManager } = useImageManager() const cropper:Ref = ref(null) const image: Ref = ref({ id: null, src: null, file: null, name: null }) (...) if(props.existingImageId){ useFetch(async () => { const result: ApiResponse = await $dataProvider.invoke({ type: QUERY_TYPE.DEFAULT, url: 'api/files', id: props.existingImageId }) const { fetchState, imageLoaded } = imageManager.fetch(props.existingImageId) const config = JSON.parse(imageLoaded.data.config) (...) }) } // je suis pas sûr de comprendre la différence entre l'appel précédent (dans le if) et celui ci? const { fetchState, imageLoaded } = getOne(props.existingImageId) (...) const save = async () => { fileToSave.config = JSON.stringify({ x: coordinates.value.left, y: coordinates.value.top, height: coordinates.value.height, width: coordinates.value.width }) //Cas d'un PUT : l'image existe déjà on bouge simplement le cropper if (image.value.id) { em.persist(image.value) //On émet un évent afin de mettre à jour le formulaire de départ emit('reload') } //Post : on créer une nouvelle image donc on passe par l'api legacy... else{ if (image.value.file) { //On créer l'objet File à sauvegarder const file = em.new(File) file.name = image.value.name file.imgFieldName = props.field file.visibility = 'EVERYBODY' file.folder = 'IMAGES' if (props.ownerId) file.ownerId = props.ownerId //Appel au datapersister em.persist(file) //On émet un évent afin de mettre à jour le formulaire de départ emit('update', response.data['@id']) } else { //On reset l'image : on a appuyer sur "poubelle" puis on enregistre emit('reset') } } } ##### 9. Téléchargement / upload de fichier _(exemple?)_ ##### 10- Affichage et éditions des notifications Dans components/Layout/Header/Notification.vue : 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 }) const notifications: ComputedRef = computed(() => { const query = repositoryHelper.getRepository(Notification).with('message').orderBy('id', 'desc') return queryHelper.getCollection(query) }) (...) const markNotificationsAsRead = () => { unreadNotification.value.map((notification:Notification)=>{ notification.notificationUsers = ['read'] repositoryHelper.persist(Notification, notification) createNewNotificationUsers(notification) }) } const createNewNotificationUsers = (notification: Notification) =>{ const newNotificationUsers = repositoryHelper.persist(NotificationUsers, new NotificationUsers( { access:`/api/accesses/${currentAccessId}`, notification:`/api/notifications/${notification.id}`, isRead: true } )) as NotificationUsers $dataPersister.invoke({ type: QUERY_TYPE.MODEL, model: NotificationUsers, idTemp: newNotificationUsers.id, showProgress: false }) } Alternative : components/Layout/Header/Notification.vue : const { em } = useEntityManager() const notifications: Ref = em.fetchAll(Notification) (...) const markNotificationsAsRead = () => { unreadNotification.value.map((notification: Notification) => { notification.notificationUsers = ['read'] const notificationUser = em.new(NotificationUsers) notificationUser.access = `/api/accesses/${currentAccessId}` notificationUser.notification = `/api/notifications/${notification.id}` notificationUser.isRead = true em.persist(notificationUser) }) } ##### 11- Validation de données Sur la page pages/organization/index.vue : const checkSiretHook = async (siret: string, field: string, updateRepository: any) => { await checkSiret(siret) if (!siretError.value) { updateRepository(siret, field) } } Dans composables/form/useValidator.ts : function useHandleSiret() { const siretError: Ref = ref(false) const siretErrorMessage: Ref = ref('') const checkSiret = async (siret: string) => { const response = await $dataProvider.invoke({ type: QUERY_TYPE.DEFAULT, url: '/api/siret-checking', id: siret }) if (typeof response !== 'undefined') { siretError.value = !response.isCorrect siretErrorMessage.value = response.isCorrect ? '' : i18n.t('siret_error') as string } else { siretError.value = false siretErrorMessage.value = '' } } return { siretError, siretErrorMessage, checkSiret } } Alternative : composables/form/useValidator.ts function useHandleSiret() { const siretError: Ref = ref(false) const siretErrorMessage: Ref = ref('') const checkSiret = async (siret: string) => { const response = await Connection.get({ url: '/api/siret-checking/' + siret, }) if (typeof response !== 'undefined') { siretError.value = !response.isCorrect siretErrorMessage.value = response.isCorrect ? '' : i18n.t('siret_error') as string } else { siretError.value = false siretErrorMessage.value = '' } } return { siretError, siretErrorMessage, checkSiret } } ##### 12- Affichage de données issues d'une api externe ou de routes custom Dans components/Ui/Template/MobytStatus.vue : const mobytStatus: Ref = ref(null) // fetch the mobyt status const { fetchState } = useFetch(async () => { try { const response:ApiResponse = await $dataProvider.invoke({ type: QUERY_TYPE.DEFAULT, url: '/api/mobyt/status/' + id }) mobytStatus.value = response.data as MobytUserStatus if(!mobytStatus.value?.active) emit('disabled_sms_row') } catch (Error) { // eslint-disable-next-line no-console console.error('Error: Mobyt status not found') } }) Même remarque que pour le 1-, ce serait mieux d'utiliser directement le service Connection : async getUserProfile ({ dispatch }) { const myProfile = await this.app.context.$connection.get('/api/my_profile') await dispatch('profile/access/setProfile', myProfile) } // fetch the mobyt status const { fetchState } = useFetch(async () => { try { const response: ApiResponse = await $connection.get('/api/mobyt/status/' + id) mobytStatus.value = response as MobytUserStatus if(!mobytStatus.value?.active) emit('disabled_sms_row') } catch (Error) { // eslint-disable-next-line no-console console.error('Error: Mobyt status not found') } })