analyse_092022.md 44 KB

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? xconst 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<Model> = repositoryHelper.getRepository(Parameters)
const query: ComputedRef<Query> = computed(() => repository.query())
const entry: ComputedRef<AnyJson> = 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<Query> = computed(() => repository.query())
const entry: ComputedRef<Item> = 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 :

<script>
  function getRepositories () {
    return {
       (...)
       contactPointRepository: repositoryHelper.getRepository(ContactPoint),
       (...)
    }
  }

  setup () {
    (...)
    return {
      repositories: () => getRepositories(),
      (...)  
    }
  }
</script>

<template>
  <UiCollection
    :query="repositories().contactPointRepository.query()"
    :root-model="models().Organization"
    :root-id="id"
    :model="models().ContactPoint"
    newLink="/organization/contact_points/new"
  >
    <template #list.item="{items}">
      (...)
    </template>
  </UiCollection>

</template>

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<Collection> = 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 :

<script>
  setup () {
    const {$dataProvider, route} = useContext()
    const {getItemToEdit} = useDataUtils($dataProvider)
    const id = parseInt(route.value.params.id)
    const {fetchState} = getItemToEdit(id, ContactPoint)
    return {
      id,
      fetchState
    }
</script>

<template>
    <FormOrganizationContactPoint :id="id" v-if="!fetchState.pending"></FormOrganizationContactPoint>
</template>

Dans components/Form/Organization/ContactPoint.vue :

<script>
  setup () {
    const repository: VuexRepository<Model> = repositoryHelper.getRepository(ContactPoint)
    const query: Query = repository.query()

    (...)

    return {
      model: ContactPoint,
      query: () => query,
      (...)
    }
  }
</script>

<template>
    <UiForm
      :id="id"
      :model="model"
      :query="query()"
      :submitActions="submitActions">
    >
      <template #form.input="{entry, updateRepository}">
        (...)
      </template>
    </UiForm>
</template>

Dans components/Ui/Form.vue :

<script>
  setup () {
    const {$dataPersister, store, app: {i18n}} = useContext()
    const {id, query}: ToRefs = toRefs(props)

    const entry: ComputedRef<AnyJson> = computed(() => {
      return queryHelper.getFlattenEntry(query.value, id.value)
    })

    (...)

    const submit = async (next: string|null = null) => {
      if(await validate()){
        markAsNotDirty()

        try {
          const response = await $dataPersister.invoke({
            type: QUERY_TYPE.MODEL,
            model: props.model,
            id: store.state.form.formStatus === FORM_STATUS.EDIT ? id.value : null,
            idTemp: store.state.form.formStatus === FORM_STATUS.CREATE ? id.value : null,
            query: props.query
          })

        (...)
   }

   const quitForm = () => {
      // reset entity
      const entryCopy = query.value.first()
      if (entryCopy && entryCopy.$getAttributes().originalState) {
        repositoryHelper.persist(props.model, entryCopy.$getAttributes().originalState)
      }
   }

   return {
    submit,
    updateRepository,
    entry,
    quitForm,
  }
</script>

<template>
  <v-form
    ref="form"
    lazy-validation
    :readonly="readonly"
  >
    (...)
    <UiButtonSubmit
      v-if="!readonly"
      @submit="submit"
      :actions="actions"
    ></UiButtonSubmit>
  </v-form>
</template>

Ce qui pourrait peut-être donner quelque chose comme :

pages/organization/contact_points/_id.vue :

<script>
  setup () {
    const id = parseInt(route.value.params.id)

    return {
      id
    }
</script>

<template>
    <FormOrganizationContactPoint :id="id"></FormOrganizationContactPoint>
</template>

components/Form/Organization/ContactPoint.vue :

<script>
  setup () {
    const { em } = useEntityManager()

    const { fetchState, contactPoint } = props.id ? em.fetch(ContactPoint, props.id) : em.new(ContactPoint)

    (...)

    return {
      contactPoint,
      fetchState,
      (...)
    }
  }
</script>

<template>
    <UiForm
      v-if="!fetchState.pending"
      :id="id"
      :model="model"
      :submitActions="submitActions">
    >
      <template #form.input="{contactPoint}">
        (...)
      </template>
    </UiForm>
</template>

Dans components/Ui/Form.vue :

<script>
  setup () {
    const { em } = useEntityManager()
    const { id }: ToRefs = toRefs(props)

    (...)

    const submit = async (next: string|null = null) => {
      if(await validate()){
        markAsNotDirty()

        em.persist(contactPoint)

        (...)
   }

   const quitForm = () => {
      // reset entity
      em.reset(contactPoint)
   }

   return {
    submit,
    contactPoint,
    quitForm,
  }
</script>

<template>
  <v-form
    ref="form"
    lazy-validation
    :readonly="readonly"
  >
    (...)
    <UiButtonSubmit
      v-if="!readonly"
      @submit="submit"
      :actions="actions"
    ></UiButtonSubmit>
  </v-form>
</template>
5- Population d'un input par un enum

Exemple sur la page components/Form/Organization/ContactPoint.vue :

<UiInputEnum
  field="contactType"
  label="contactpoint_type"
  :data="entry['contactType']"
  enum-type="contact_point_type"
  @update="updateRepository"
/>

Dans components/Ui/Input/Enum.vue :

const { enumType } = props
const { $dataProvider, store } = useContext()
(...)

const items: Ref<Array<EnumChoices>> = 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<Array<EnumChoices>> = 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 :

<script>

    const {countries, fetchState: countriesFetchingState} = useCountryProvider($dataProvider)

    return {
      (...)
      countries,
      countriesFetchingState,
      (...)
    }
</script>

<template>
  <UiInputAutocomplete
    field="addressPostal.addressCountry"
    label="addressCountry"
    :data="getIdFromUri(entry['addressPostal.addressCountry'])"
    :items="countries"
    :isLoading="countriesFetchingState.pending"
    :item-text="['name']"
    @update="updateRepository(`/api/countries/${$event}`, 'addressPostal.addressCountry')"
  />
</template>

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 :

<script>

    const { em } = useEntityManager()

    const { countriesFetchingState, countries } = em.fetchAll(Country)

    return {
      (...)
      countries,
      countriesFetchingState,
      (...)
    }
</script>
7- Population d'un input depuis une source externe (ex : adresses)

Exemple sur la page components/Form/Organization/Address.vue :

<script>
  const {searchFunction: addressSearch, updateCpAddress} = useAddressPostalUtils($dataProvider)

  return {
    (...)
    addressSearch,
    updateCpAddress,
    (...) 
  }
</script

<template>
  (...)

  <UiInputAutocompleteWithAPI
    field="owner"
    label="importAddress"
    :item-text="['person.givenName', 'person.name']"
    :searchFunction="accessSearch"
    @update="updateAccessAddress"
  />

  (...)

  <UiInputAutocompleteWithAPI
    field="addressPostal.postalCode"
    label="postalCode"
    :data="getAutoCompleteAddressItem"
    :item-text="['postcode']"
    :slotText="['postcode', 'city']"
    :searchFunction="addressSearch"
    :returnObject="true"
    @update="updateCpAddress($event, updateRepository)"
  />

  <UiInputAutocompleteWithAPI
    field="addressPostal.addressCity"
    label="addressCity"
    :data="getAutoCompleteAddressItem"
    :item-text="['city']"
    :slotText="['postcode', 'city']"
    :searchFunction="addressSearch"
    :returnObject="true"
    @update="updateCpAddress($event, updateRepository)"
  />

  (...)
</template>

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<Array<AnyJson>> {
  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<Array<AnyJson>> {
  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 :

<UiImage
  :id="getIdFromUri(entry['logo'])"
  :upload="true"
  :width="200"
  field="logo"
  :ownerId="id"
  @update="updateRepository"
></UiImage>

Dans components/Ui/Image.vue :

<script>
  const {getOne, provideImg} = useImageProvider($dataProvider, $config)

  const { imageLoaded, fetch } = getOne(props.id, props.imageByDefault, props.height, props.width)
</script>

<template>
  <v-img
    :src="imgSrcReload ? imgSrcReload : imageLoaded"
    :lazy-src="require(`/assets/images/byDefault/${imageByDefault}`)"
    :height="height"
    :width="width"
    aspect-ratio="1"
  >(...)</v-img>

  <UiInputImage
    v-if="openUpload"
    @close="openUpload=false"
    :existingImageId="id"
    :field="field"
    :ownerId="ownerId"
    @update="$emit('update', $event, field); openUpload=false"
    @reload="fetch();openUpload=false"
    @reset="reset"
  ></UiInputImage>
</template>

Dans components/Ui/Input/Image.vue :

const {$dataProvider, $config, $dataPersister} = useContext()
const {getOne} = useImageProvider($dataProvider, $config)

const fileToSave = new File()
const cropper:Ref<any> = ref(null)
const image: Ref<AnyJson> = 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 :

<UiImage
  :id="getIdFromUri(organization['logo'])"
  :upload="true"
  :width="200"
  field="logo"
  :ownerId="id"
></UiImage>

components/Ui/Image.vue :

<script>
  const { imageManager } = useImageManager()

  const { imageLoaded, image } = imageManager.fetch(props.id, props.height, props.width)
</script>

components/Ui/Input/Image.vue :

const { em } = useEntityManager()
const { imageManager } = useImageManager()

const cropper:Ref<any> = ref(null)
const image: Ref<AnyJson> = 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<boolean> = ref(false)
  const siretErrorMessage: Ref<string> = 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<boolean> = ref(false)
  const siretErrorMessage: Ref<string> = 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<MobytUserStatus | null> = 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')
  }
})