597101262d
Auto Tag Develop / tag (push) Successful in 8s
## Contexte Résout **ERP-107** — pendant back du mapping d'erreur par champ front (ERP-101). Le front (`useFormErrors` / `mapViolationsToRecord`) affiche sous chaque champ le `message` renvoyé par le back. Ce ticket garantit que ces messages existent, sont en FR et rattachés au bon champ. ## Changements - **Messages FR explicites** sur toutes les contraintes `#[Assert\*]` des entités métier : `Client`, `ClientContact`, `ClientAddress`, `ClientRib`, `Category`, `Role`, `User` (Email, NotBlank, Length, Bic, Iban, PositiveOrZero, Count…). - **Contraintes `Assert\Length` manquantes ajoutées**, calées sur le `length` de la colonne ORM (téléphones `VARCHAR(20)`, `siren`, `nTva`, `accountNumber`, `username`…). Évite une erreur Postgres 500 non rattachée au champ → 422 propre. - **Locale FR globale** (`symfony/translation` + `default_locale: fr`) comme filet pour les messages natifs Symfony non surchargés. - **Garde-fou** `tests/Architecture/EntityConstraintsHaveFrenchMessageTest` : échoue si une contrainte n'a pas de message FR explicite (comparaison au défaut Symfony) ou si `Assert\Length.max` diverge du `length` ORM. Whitelist justifiée pour les formats auto-bornés (Bic/Iban/Regex CP/couleur hex). - **Test fonctionnel** du JSON 422 réel : message FR + `propertyPath` consommable par le front. - **Convention documentée** dans `.claude/rules/backend.md`. ## Décisions - Stratégie retenue : message FR **explicite sur toutes** les contraintes + locale FR en filet (les deux leviers du ticket). - Garde-fou `Length == ORM length` : **test bloquant** (anti-dérive). - RG-1.03 (distributor/broker) : pas de `Assert\Callback` ajouté — le `ClientProcessor` gère **déjà** l'exclusivité (422 + `propertyPath`). Pas de doublon. ## Hors périmètre / à suivre - **Alignement `nullable`(DB) / `NotBlank`(back) / `required`(front)** : les champs obligatoires existants ont été confirmés, mais aucun changement de nullabilité DB n'a été fait sans arbitrage métier. À recroiser avec les astérisques front (ERP-101 / PR #58) si divergence constatée. ## Vérifications - `make test` : **469 tests verts** (1793 assertions), 0 échec/erreur. - `php-cs-fixer` : 0 fichier à corriger. --------- Co-authored-by: admin malio <malio@yuno.malio.fr> Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #59 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
163 lines
9.2 KiB
Markdown
163 lines
9.2 KiB
Markdown
# Frontend — Regles Nuxt 4 / Vue 3 / @malio/layer-ui
|
|
|
|
## Base
|
|
|
|
- TypeScript strict
|
|
- 4 espaces d'indentation
|
|
- Commentaires (JSDoc, inline, bloc) **en francais** ; code (variables, types) en anglais
|
|
- Chaque module front = un layer Nuxt auto-detecte (`frontend/modules/*/nuxt.config.ts` minimal)
|
|
|
|
## Appels API
|
|
|
|
- Toujours `useApi()` — jamais `$fetch`, `ofetch`, `axios` en direct
|
|
- `useApi()` gere : cookies JWT, erreurs, toasts i18n, parsing Hydra
|
|
|
|
## Stores (Pinia)
|
|
|
|
- `useAuthStore` pour l'authentification
|
|
- `useUiStore` pour l'etat UI global (sidebar, modales, etc.)
|
|
- Composables avec state singleton (refs module-level) : exposer une fonction `reset*()` et la rappeler au logout (ex: `useSidebar().resetSidebar()`)
|
|
|
|
## Middlewares globaux
|
|
|
|
- `auth.global.ts` protege les routes + charge la sidebar apres login
|
|
- `modules.global.ts` redirige si la route demandee est dans `disabledRoutes`
|
|
|
|
## i18n et sidebar
|
|
|
|
- Labels de sidebar = cles i18n `sidebar.<module>.*`, jamais du texte brut
|
|
- Le layout `default.vue` applique `t()` sur les labels retournes par `/api/sidebar`
|
|
- Traductions dans `frontend/i18n/locales/`
|
|
|
|
## Composants formulaires — @malio/layer-ui obligatoire
|
|
|
|
Tout champ de formulaire / filtre doit utiliser les composants `Malio*` plutot que `<input>` / `<select>` bruts :
|
|
|
|
- `MalioInputText`, `MalioInputNumber`, `MalioInputAmount`, `MalioInputPassword`, `MalioInputTextArea`
|
|
- `MalioSelect`, `MalioSelectCheckbox`, `MalioCheckbox`, `MalioRadioButton`
|
|
- `MalioInputUpload`, `MalioTime`
|
|
- `MalioButton`, `MalioButtonIcon`
|
|
|
|
**Exceptions autorisees** (commenter un `// TODO` pour migrer quand la lib couvrira le cas) :
|
|
1. Type non couvert : `datetime-local`, `date`, color picker, file drag & drop
|
|
2. Bug connu bloquant (ex: `MalioSelect` avec options string) — documenter le bug en commentaire
|
|
|
|
Toute autre exception requiert validation avant merge.
|
|
|
|
## Validation des formulaires — useFormErrors obligatoire (erreur par champ)
|
|
|
|
**Tout formulaire qui soumet a une API DOIT afficher les erreurs de validation 422 sous le champ concerne, via `useFormErrors`** (`frontend/shared/composables/useFormErrors.ts`). C'est le pendant front de « le back renvoie TOUTES les violations d'une 422 d'un coup » : un seul aller-retour, chaque erreur affichee inline sous son champ (prop `:error` des `Malio*`), pas un toast fourre-tout.
|
|
|
|
Principe cle : **le nom du champ cote front = le `propertyPath` renvoye par le back**. Aucun mapping manuel champ par champ.
|
|
|
|
Pattern de reference (champs scalaires) :
|
|
|
|
```ts
|
|
const { errors, setError, clearErrors, handleApiError } = useFormErrors()
|
|
|
|
async function submit() {
|
|
clearErrors()
|
|
try {
|
|
await useApi().post('/clients', payload, { toast: false }) // toast: false obligatoire
|
|
} catch (e) {
|
|
// 422 → mapping inline par champ (pas de toast) ; autre → toast de fallback.
|
|
handleApiError(e, { fallbackMessage: t('foo.error') })
|
|
}
|
|
}
|
|
```
|
|
|
|
```vue
|
|
<MalioInputText v-model="form.companyName" :error="errors.companyName" />
|
|
<MalioSelect v-model="form.siren" :error="errors.siren" />
|
|
```
|
|
|
|
Regles :
|
|
- **Toujours `{ toast: false }`** sur l'appel API qui veut un mapping inline (sinon le toast natif d'`useApi` masque le fin).
|
|
- **Cas metier specifique** (ex: 409 doublon) : `setError('champ', message)` + toast explicite **avant** de deleguer le reste a `handleApiError`. Cf. `useCategoryForm` (doublon RG-1.07).
|
|
- **Collections** (listes de sous-entites sauvees par un appel par ligne) : une erreur PAR LIGNE via un tableau `ref<Record<string, string>[]>` aligne sur l'index, peuple par `mapViolationsToRecord(error.response._data)` (util pur de `shared/utils/api.ts`). Le composant de ligne expose une prop `:errors` (`Record<string, string>`) bindee sur le `:error` de chaque champ. Cf. `ClientContactBlock` / `ClientAddressBlock` et les submits de `clients/new.vue` / `clients/[id]/edit.vue`.
|
|
|
|
**Interdit** : se contenter d'un toast global sur une 422 quand le back identifie les champs fautifs (`propertyPath`). Reimplementer un mapping `if/else` par champ a la main au lieu d'`useFormErrors` / `mapViolationsToRecord`.
|
|
|
|
## Tableaux de donnees — MalioDataTable obligatoire
|
|
|
|
Tout affichage LISTE tabulaire (donnees metier paginees, CRUD admin) doit passer par `MalioDataTable` :
|
|
- Pagination integree
|
|
- Slots `#header-*` pour filtres, `#cell-*` pour rendu custom
|
|
- Pas de `<table>` brut avec pagination custom
|
|
|
|
**Exception** : tableaux purement presentationnels non paginables (diff field/old/new, grille de comparaison, matrice RBAC d'admin, etc.) peuvent rester en `<table>` HTML brut.
|
|
|
|
## Listes paginees (standard) — usePaginatedList obligatoire
|
|
|
|
**Toute liste qui consomme une `GetCollection` API doit passer par `usePaginatedList`** (`frontend/shared/composables/usePaginatedList.ts`). Le composable est le pendant front de la regle ABSOLUE n°13 (« toute collection est paginee cote back ») : il consomme l'envelope Hydra (`member` / `totalItems` / `view`) et expose un etat reactif a brancher directement sur `MalioDataTable`.
|
|
|
|
Pattern de reference :
|
|
|
|
```ts
|
|
const {
|
|
items,
|
|
totalItems,
|
|
currentPage,
|
|
itemsPerPage,
|
|
itemsPerPageOptions,
|
|
fetch: loadList,
|
|
goToPage,
|
|
setItemsPerPage,
|
|
} = usePaginatedList<MyEntity>({ url: '/my-resources' })
|
|
|
|
onMounted(loadList)
|
|
```
|
|
|
|
```vue
|
|
<MalioDataTable
|
|
:columns="columns"
|
|
:items="rows"
|
|
:total-items="totalItems"
|
|
:page="currentPage"
|
|
:per-page="itemsPerPage"
|
|
:per-page-options="itemsPerPageOptions"
|
|
:empty-message="t('foo.empty')"
|
|
@update:page="goToPage"
|
|
@update:per-page="setItemsPerPage"
|
|
/>
|
|
```
|
|
|
|
Garanties offertes par le composable :
|
|
- Force `Accept: application/ld+json` → API Platform 4 renvoie bien `member` / `totalItems` (sans Accept, retour tableau plat sans pagination).
|
|
- Defaut 10 items/page, choix client 10 / 25 / 50, aligne sur le defaut serveur.
|
|
- Mutation `setFilters` / `setSort` / `setItemsPerPage` → retombe systematiquement en page 1.
|
|
- Cas limite « page hors borne apres filtre » : retombe automatiquement sur la derniere page valide (`tests/usePaginatedList.test.ts`).
|
|
- Etat 100 % local (refs internes a l'instance) — **jamais reflete dans l'URL**, conformement a la regle « Etat des tableaux — pas de persistance URL » ci-dessous.
|
|
|
|
A NE PAS faire :
|
|
- Charger une collection complete via `?itemsPerPage=999` pour bypasser la pagination. Le seul cas legitime de retour complet est l'alimentation d'un `<select>` sur un referentiel ≤ quelques dizaines d'entrees, et il passe par `?pagination=false` (echappatoire prevue par `pagination_client_enabled: true`).
|
|
- Reimplementer la pagination prev/next a la main au-dessus de `MalioDataTable` — le composant porte deja le selecteur items/page et les boutons Prev/Next.
|
|
- Persister `page`/`tri`/`filtre` dans la query string — meme regle que pour `<MalioDataTable>` brut (cf. section suivante).
|
|
|
|
## Etat des tableaux — pas de persistance URL
|
|
|
|
**Interdit** de persister l'etat d'un tableau (filtres, pagination, tri par colonne, selection, ligne active, scroll) dans la query string ou de le reinjecter depuis `route.query` au montage.
|
|
|
|
- L'etat vit uniquement dans le composant (`reactive`, `ref` locales)
|
|
- Seuls les deep links "de navigation metier" (ex: ouvrir un detail precis `/users/42`) sont dans l'URL
|
|
- Exceptions autorisees **sur demande explicite** de l'utilisateur
|
|
|
|
## Validation des formulaires (standard ERP-101)
|
|
|
|
Regle transverse a TOUS les formulaires front (et a rappeler a l'ecriture de chaque ticket back/front portant un formulaire). Decidee en ERP-101 (declencheur : ecran « Ajouter un client » ERP-63).
|
|
|
|
- **Champs obligatoires** : prop `required` du composant `Malio*` + etoile (asterisque) rouge dans le label. Ne JAMAIS griser le bouton « Valider » sans feedback : bouton toujours actif + erreurs affichees sous les champs.
|
|
- **Couche de validation autoritaire = le back** : les RG sont re-validees serveur (mode strict). Au `422`, mapper `violations[].propertyPath` vers la prop `error` du champ via `extractApiViolations` (deja utilise par `useCategoryForm`). Zero duplication de RG, zero drift.
|
|
- **Feedback instantane au blur** : uniquement requis / min / max / format (pas de re-implementation des RG metier cote front).
|
|
- **Regles front-only** : celles sans equivalent back (ex. FK nullable cote back mais obligatoire selon un choix UI) sont validees et affichees cote front.
|
|
- **Email — PAS de masque** : un email n'a pas de structure fixe. Normalisation via la prop `lowercase` de `MalioInputEmail` (trim + suppression des espaces + lowercase, coherent avec la normalisation serveur RG-1.21). Le format est valide par la prop `error` (violations serveur ou check au blur), jamais par un masque. Retirer tout shaping email ad hoc des ecrans.
|
|
- **Contrat back attendu** : tout `422` issu d'un Processor/Validator doit porter `violations[].propertyPath` aligne sur les noms de champs du formulaire, pour etre consommable par `extractApiViolations`.
|
|
- **Dependance** : le branchement des props `required` suppose `@malio/layer-ui` a jour (props `required` + etoile — MUI-41 / ERP-101).
|
|
|
|
## Interdits
|
|
|
|
- `modules-loader.ts`, `.module.ts` — le scan des layers est automatique
|
|
- Hardcode de la sidebar cote front — elle vient de `/api/sidebar`
|
|
- Edition manuelle de `extends` dans `frontend/nuxt.config.ts` — les layers sont scannes
|
|
- Import d'un module front depuis un autre module — passer par `frontend/shared/`
|