a340d8139a
Auto Tag Develop / tag (push) Successful in 8s
## Contexte ERP-148 — mise à jour @malio/layer-ui et amélioration des champs date (onglet Information, Client & Fournisseur). ## Changements - **MalioDate v1.7.10** : le composant expose désormais son état de validité (`@update:valid`) et la saisie brute invalide (`@update:rawValue`). - **Validation back-autoritaire du format** : `foundedAt` n'accepte plus que l'ISO strict `Y-m-d` (`#[Context]` DateTimeNormalizer) + `collectDenormalizationErrors` sur `Client` et `Supplier`. Toute saisie non-ISO renvoie un **422 porté sur le champ**. - Corrige un cas piège : `12/25/2026` (invalide en JJ/MM/AAAA côté front) était auparavant accepté par PHP en M/J/AAAA → 25 décembre. Désormais rejeté. - **Front** : la saisie invalide est transmise au back ; le message technique de type-error est surchargé par une clé i18n via le **code de violation** (`resolveViolationMessage` / `VIOLATION_MESSAGE_I18N`), affiché inline par `useFormErrors`. - Réorganisation des utils de formulaire sous `utils/forms/`. ## Tests - Back : `ClientFoundedAtFormatTest` / `SupplierFoundedAtFormatTest` (dont le cas piège `12/25/2026`). - Front : résolveur i18n (`api.test.ts`, `useFormErrors.test.ts`) + payloads (`clientEdit`/`supplierEdit` specs). - Suite Commercial verte ; vérifié bout-en-bout en navigateur (PATCH → 422, erreur inline, submit bloqué). ## Note Échecs JWT aléatoires connus du hook pre-commit (401/500 sur tests d'auth sans rapport) ; tous verts en isolation. Reviewed-on: #92 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
168 lines
11 KiB
Markdown
168 lines
11 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.
|
|
|
|
## Standard ecran formulaire — reference : ecran Client
|
|
|
|
**Tout nouvel ecran de formulaire doit ressembler au premier ecran Client** (`frontend/modules/commercial/pages/clients/new.vue` + `[id]/edit.vue`) : meme structure (bloc principal puis onglets), memes marges/espacements, memes blocs de collection (ajout/suppression inline), meme validation inline 422 par champ. C'est la reference visuelle et fonctionnelle des formulaires du projet — s'en inspirer avant d'en creer un nouveau.
|
|
|
|
## 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`.
|
|
- **Message back technique → surcharge i18n par code** : la plupart des contraintes back portent un message FR explicite (affiche tel quel). Mais une 422 peut porter un message TECHNIQUE non montrable (ex. erreur de type API Platform sur une date non parsable : « Cette valeur doit être de type DateTimeImmutable|null. », voire en anglais selon la negociation). On le surcharge **cote front** via le `code` de violation (UUID Symfony fige, robuste — pas un match sur le texte) : table `VIOLATION_MESSAGE_I18N` + `resolveViolationMessage` dans `shared/utils/api.ts`, appliquee par `useFormErrors`. Ajouter un cas = une entree `code -> cle i18n`. Cas reference : date invalide (MalioDate forwarde la saisie brute via `@update:rawValue`, le back renvoie 422 sur `foundedAt` grace a `collectDenormalizationErrors`, le front affiche `errors.validation.invalidDate`).
|
|
|
|
**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/`
|