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>
156 lines
6.2 KiB
TypeScript
156 lines
6.2 KiB
TypeScript
/**
|
|
* Schemas Hydra / API Platform 4.
|
|
*
|
|
* Important : API Platform 4 abandonne le prefixe `hydra:` dans les noms de
|
|
* proprietes (compare a la version 3). Un GET /api/audit-logs renvoie :
|
|
* { "@context": ..., "@id": ..., "@type": "...",
|
|
* "member": [...],
|
|
* "totalItems": 30,
|
|
* "view": { "@id": ..., "@type": "...", "first": ..., "next": ..., ... } }
|
|
*
|
|
* En `application/json` (sans ld), API Platform retourne un simple tableau
|
|
* plat sans ces metadonnees — on doit donc explicitement demander
|
|
* `application/ld+json` (via l'option `headers: { Accept: ... }` de useApi)
|
|
* pour avoir acces a la pagination.
|
|
*/
|
|
export interface HydraView {
|
|
'@id'?: string
|
|
'@type'?: string
|
|
first?: string
|
|
last?: string
|
|
next?: string
|
|
previous?: string
|
|
}
|
|
|
|
export interface HydraCollection<T> {
|
|
member: T[]
|
|
totalItems: number
|
|
view?: HydraView
|
|
}
|
|
|
|
export function extractHydraMembers<T>(collection: HydraCollection<T>): T[] {
|
|
return collection.member ?? []
|
|
}
|
|
|
|
/**
|
|
* Une violation de contrainte API Platform (reponse 422). Le `propertyPath`
|
|
* pointe le champ concerne, `message` est le libelle a afficher, `code` est le
|
|
* code de contrainte Symfony (UUID stable, independant de la langue) — il sert
|
|
* a surcharger un message back technique par une cle i18n (cf.
|
|
* `VIOLATION_MESSAGE_I18N` / `resolveViolationMessage`).
|
|
*/
|
|
export interface ApiViolation {
|
|
propertyPath: string
|
|
message: string
|
|
code: string
|
|
}
|
|
|
|
/**
|
|
* Extrait les violations d'un payload d'erreur 422 d'API Platform 4. Supporte
|
|
* les deux formats de negociation (`violations` ou `hydra:violations`) et
|
|
* renvoie un tableau vide si le payload n'en contient pas d'exploitables.
|
|
*
|
|
* Utilise par useCategoryForm et tout futur composable de formulaire qui
|
|
* doit mapper les violations serveur sur ses champs.
|
|
*/
|
|
export function extractApiViolations(data: unknown): ApiViolation[] {
|
|
if (!data || typeof data !== 'object') return []
|
|
const record = data as Record<string, unknown>
|
|
const raw = record.violations ?? record['hydra:violations']
|
|
if (!Array.isArray(raw)) return []
|
|
const out: ApiViolation[] = []
|
|
for (const v of raw) {
|
|
if (!v || typeof v !== 'object') continue
|
|
const obj = v as Record<string, unknown>
|
|
out.push({
|
|
propertyPath: String(obj.propertyPath ?? ''),
|
|
message: String(obj.message ?? ''),
|
|
code: String(obj.code ?? ''),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Transforme un payload d'erreur 422 d'API Platform en dictionnaire
|
|
* `{ propertyPath: message }`, directement consommable par la prop `:error`
|
|
* des composants `Malio*` (le nom du champ cote front = le `propertyPath`
|
|
* renvoye par le back). Fondation du mapping erreur→champ des formulaires :
|
|
* utilise par `useFormErrors` (champs scalaires) et par les boucles de submit
|
|
* de collections (erreur par ligne).
|
|
*
|
|
* Les violations sans `propertyPath` (erreur globale) sont ignorees ; en cas
|
|
* de doublon de `propertyPath`, la derniere violation l'emporte.
|
|
*/
|
|
export function mapViolationsToRecord(data: unknown): Record<string, string> {
|
|
const out: Record<string, string> = {}
|
|
for (const v of extractApiViolations(data)) {
|
|
if (v.propertyPath) out[v.propertyPath] = v.message
|
|
}
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Surcharge i18n d'un message back par CODE de violation.
|
|
*
|
|
* La plupart des contraintes back portent deja un message FR explicite (ex.
|
|
* `#[Assert\NotBlank(message: '...')]`) : on l'affiche tel quel. Mais certaines
|
|
* 422 portent un message TECHNIQUE non montrable a l'utilisateur — typiquement
|
|
* l'erreur de TYPE renvoyee par API Platform quand le back ne peut pas
|
|
* denormaliser la valeur (date non parsable envoyee sur un champ
|
|
* `DateTimeImmutable` : « Cette valeur doit être de type DateTimeImmutable|null. »,
|
|
* voire en anglais selon la negociation de langue).
|
|
*
|
|
* Plutot que de traduire/maquiller cote back, on surcharge ces messages cote
|
|
* front via leur `code` de violation. Ce code est un UUID Symfony FIGE (contrat
|
|
* de compatibilite : il ne change pas entre versions), donc bien plus robuste
|
|
* qu'un match sur le texte du message (qui depend de la langue). La table
|
|
* associe un code -> une cle i18n ; `resolveViolationMessage` l'applique.
|
|
*
|
|
* Limite a connaitre : le code de type-error est GENERIQUE (toute valeur de
|
|
* mauvais type). Dans nos formulaires, seul un champ date saisi en texte libre
|
|
* (MalioDate, qui forwarde la saisie brute invalide) le declenche, d'ou le
|
|
* libelle « Date invalide ». Si un autre champ typé en saisie libre apparait,
|
|
* affiner la resolution via `propertyPath` plutot que par code seul.
|
|
*/
|
|
export const VIOLATION_MESSAGE_I18N: Record<string, string> = {
|
|
// Symfony `Constraints\Type::INVALID_TYPE_ERROR` — valeur de mauvais type.
|
|
'ba785a8c-82cb-4283-967c-3cf342181b40': 'errors.validation.invalidDate',
|
|
}
|
|
|
|
/**
|
|
* Resout le message a afficher pour une violation : si son `code` est surcharge
|
|
* par `VIOLATION_MESSAGE_I18N`, renvoie la traduction de la cle associee ;
|
|
* sinon, le message back tel quel (cas nominal). `t` est passe par l'appelant
|
|
* (les utils sont purs, sans acces a useI18n).
|
|
*/
|
|
export function resolveViolationMessage(v: ApiViolation, t: (key: string) => string): string {
|
|
const i18nKey = VIOLATION_MESSAGE_I18N[v.code]
|
|
return i18nKey ? t(i18nKey) : v.message
|
|
}
|
|
|
|
/**
|
|
* Extrait un message d'erreur lisible depuis un payload Hydra / JSON
|
|
* d'erreur API Platform. Essaie les champs courants dans l'ordre :
|
|
* `hydra:description` → `detail` → `description` → `message` → `error` →
|
|
* `title` → `hydra:title`. Renvoie '' si rien d'exploitable.
|
|
*
|
|
* Si `data` est une string, la renvoie telle quelle (cas des erreurs
|
|
* Symfony en text/plain ou des messages bruts).
|
|
*/
|
|
export function extractApiErrorMessage(data: unknown): string {
|
|
if (typeof data === 'string') return data
|
|
if (!data || typeof data !== 'object') return ''
|
|
const record = data as Record<string, unknown>
|
|
return (
|
|
(record['hydra:description'] as string)
|
|
?? (record.detail as string)
|
|
?? (record.description as string)
|
|
?? (record.message as string)
|
|
?? (record.error as string)
|
|
?? (record.title as string)
|
|
?? (record['hydra:title'] as string)
|
|
?? ''
|
|
)
|
|
}
|