From a5b06de92af78123d03ba686ccf789ee876efbe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 18 Sep 2025 16:12:05 +0200 Subject: [PATCH] Refactor validation handling in `PersonEdit.vue`: Replace `hasValidationError` and `validationError` with `hasViolation` and `violationTitles`. Introduce `hasViolationWithParameter` and `violationTitlesWithParameter` for enhanced field validation. Update `RequiredIdentifierConstraint` messages, improve API error mapping, and refine `ValidationException` structure with `violationsList`. Add tests and translations for identifier validation. --- .../Resources/public/lib/api/apiMethods.ts | 24 ++--- .../ChillPersonBundle/Entity/Person.php | 2 + .../RequiredIdentifierConstraint.php | 3 +- .../RequiredIdentifierConstraintValidator.php | 1 - .../Resources/public/vuejs/_api/OnTheFly.ts | 4 + .../vuejs/_components/OnTheFly/PersonEdit.vue | 90 +++++++++++++------ ...uiredIdentifierConstraintValidatorTest.php | 6 +- .../ChillPersonBundle/config/services.yaml | 3 + .../translations/validators.fr.yml | 3 + 9 files changed, 92 insertions(+), 44 deletions(-) diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts index 79927cc4d..8a67e3d16 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts @@ -25,14 +25,13 @@ export interface TransportExceptionInterface { name: string; } -// Strict : uniquement les clés déclarées dans M[K] export type ViolationFromMap< M extends Record>, > = { [K in Extract]: { propertyPath: K; title: string; - parameters?: M[K]; // ← uniquement ces clés (pas d’extras) + parameters?: M[K]; type?: string; }; }[Extract]; @@ -53,18 +52,19 @@ export interface ValidationExceptionInterface< >, > extends Error { name: "ValidationException"; - /** Copie du payload serveur (utile pour logs/diagnostic) */ - problem: ValidationProblemFromMap; - /** Liste compacte "Titre: chemin" */ + /** Full server payload copy */ + problems: ValidationProblemFromMap; + /** A list of all violations, with property key */ + violationsList: ViolationFromMap[]; + /** Compact list "Title: path" */ violations: string[]; - /** Uniquement les titres */ + /** Only titles */ titles: string[]; - /** Uniquement les chemins de propriété */ + /** Only property paths */ propertyPaths: Extract[]; - /** Indexation par propriété (utile pour afficher par champ) */ + /** Indexing by property (useful for display by field) */ byProperty: Record, string[]>; } - export class ValidationException< M extends Record> = Record< string, @@ -75,8 +75,9 @@ export class ValidationException< implements ValidationExceptionInterface { public readonly name = "ValidationException" as const; - public readonly problem: ValidationProblemFromMap; + public readonly problems: ValidationProblemFromMap; public readonly violations: string[]; + public readonly violationsList: ViolationFromMap[]; public readonly titles: string[]; public readonly propertyPaths: Extract[]; public readonly byProperty: Record, string[]>; @@ -86,8 +87,9 @@ export class ValidationException< super(message); Object.setPrototypeOf(this, new.target.prototype); - this.problem = problem; + this.problems = problem; + this.violationsList = problem.violations; this.violations = problem.violations.map( (v) => `${v.title}: ${v.propertyPath}`, ); diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index 09b52b2ce..82224d40f 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -35,6 +35,7 @@ use Chill\PersonBundle\Entity\Person\PersonCenterCurrent; use Chill\PersonBundle\Entity\Person\PersonCenterHistory; use Chill\PersonBundle\Entity\Person\PersonCurrentAddress; use Chill\PersonBundle\Entity\Person\PersonResource; +use Chill\PersonBundle\PersonIdentifier\Validator\RequiredIdentifierConstraint; use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential; use Chill\PersonBundle\Validator\Constraints\Person\Birthdate; use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter; @@ -273,6 +274,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI private ?int $id = null; #[ORM\OneToMany(mappedBy: 'person', targetEntity: PersonIdentifier::class, cascade: ['persist', 'remove'], orphanRemoval: true)] + #[RequiredIdentifierConstraint] private Collection $identifiers; /** diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php index 872dd8113..68a75c7cf 100644 --- a/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraint.php @@ -16,9 +16,10 @@ use Symfony\Component\Validator\Constraint; /** * Test that the required constraints are present. */ +#[\Attribute] class RequiredIdentifierConstraint extends Constraint { - public string $message = 'This identifier must be set'; + public string $message = 'person_identifier.This identifier must be set'; public function getTargets(): string { diff --git a/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraintValidator.php b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraintValidator.php index c8f4ea2a6..240fe3a33 100644 --- a/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraintValidator.php +++ b/src/Bundle/ChillPersonBundle/PersonIdentifier/Validator/RequiredIdentifierConstraintValidator.php @@ -45,7 +45,6 @@ final class RequiredIdentifierConstraintValidator extends ConstraintValidator $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $worker->renderAsString($identifier)) ->setParameter('definition_id', (string) $worker->getDefinition()->getId()) - ->atPath('identifiers') ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05') ->addViolation(); } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts index fe018edc7..382d4c8a7 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.ts @@ -73,6 +73,10 @@ export interface WritePersonViolationMap "{{ value }}": string | null; }; birthdate: {}; + identifiers: { + "{{ value }}": string | null; + "definition_id": string; + }; } export const createPerson = async (person: PersonWrite): Promise => { return makeFetch( diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/PersonEdit.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/PersonEdit.vue index ad99ccaee..4c77a0c23 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/PersonEdit.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/PersonEdit.vue @@ -5,7 +5,7 @@
{{ err }} @@ -40,7 +40,7 @@
{{ err }} @@ -94,6 +94,7 @@
+
+ {{ err }} +
@@ -111,7 +118,7 @@
@@ -154,7 +161,7 @@
{{ err }} @@ -167,7 +174,7 @@
{{ trans(BIRTHDATE) }}
{{ err }} @@ -223,7 +230,7 @@
{{ trans(PERSON_MESSAGES_PERSON_PHONENUMBER) }}
{{ err }} @@ -248,7 +255,7 @@
{{ trans(PERSON_MESSAGES_PERSON_MOBILENUMBER) }}
{{ err }} @@ -273,7 +280,7 @@
{{ trans(PERSON_MESSAGES_PERSON_EMAIL) }}
{{ err }} @@ -603,19 +610,49 @@ function addQueryItem(field: "lastName" | "firstName", queryItem: string) { } type WritePersonViolationKey = Extract; -const validationErrors = ref>>({}); +const violationsList = ref["violationsList"]>([]); +function violationTitles

(property: P): string[] { + return violationsList.value.filter((v) => v.propertyPath === property).map((v) => v.title); -function validationError( - property: WritePersonViolationKey, +} +function violationTitlesWithParameter< + P extends WritePersonViolationKey, + Param extends Extract +>( + property: P, + with_parameter: Param, + with_parameter_value: WritePersonViolationMap[P][Param], ): string[] { - return validationErrors.value[property] ?? []; + const list = violationsList.value.filter((v) => v.propertyPath === property); + + const filtered = list.filter( + (v): boolean => + !!v.parameters && + // `with_parameter in v.parameters` check indexing + with_parameter in v.parameters && + // the cast is safe, because we have overloading that bind the types + (v.parameters as WritePersonViolationMap[P])[with_parameter] === with_parameter_value + ); + return filtered.map((v) => v.title); + + // Sinon, retourner simplement les titres de la propriété + return list.map((v) => v.title); } -function hasValidationError( - property: WritePersonViolationKey, + +function hasViolation

(property: P): boolean { + return violationTitles(property).length > 0; +} +function hasViolationWithParameter< + P extends WritePersonViolationKey, + Param extends Extract +>( + property: P, + with_parameter: Param, + with_parameter_value: WritePersonViolationMap[P][Param], ): boolean { - return validationError(property).length > 0; + return violationTitlesWithParameter(property, with_parameter, with_parameter_value).length > 0; } function submitNewAddress(payload: { addressId: number }) { @@ -630,8 +667,7 @@ async function postPerson(): Promise { emit("onPersonCreated", { person: createdPerson }); } catch (e: unknown) { if (isValidationException(e)) { - console.log(e.byProperty); - validationErrors.value = e.byProperty; + violationsList.value = e.violationsList; } else { toast.error(trans(PERSON_EDIT_ERROR_WHILE_SAVING)); } diff --git a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php index 31df89315..86201b6fb 100644 --- a/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/PersonIdentifier/Validator/RequiredIdentifierConstraintValidatorTest.php @@ -106,8 +106,7 @@ final class RequiredIdentifierConstraintValidatorTest extends ConstraintValidato $this->validator->validate($collection, new RequiredIdentifierConstraint()); - $this->buildViolation('This identifier must be set') - ->atPath('property.path.identifiers') + $this->buildViolation('person_identifier.This identifier must be set') ->setParameter('{{ value }}', '') ->setParameter('definition_id', '1') ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05') @@ -123,8 +122,7 @@ final class RequiredIdentifierConstraintValidatorTest extends ConstraintValidato $this->validator->validate($collection, new RequiredIdentifierConstraint()); - $this->buildViolation('This identifier must be set') - ->atPath('property.path.identifiers') + $this->buildViolation('person_identifier.This identifier must be set') ->setParameter('{{ value }}', ' ') ->setParameter('definition_id', '1') ->setCode('c08b7b32-947f-11f0-8608-9b8560e9bf05') diff --git a/src/Bundle/ChillPersonBundle/config/services.yaml b/src/Bundle/ChillPersonBundle/config/services.yaml index 9deb05944..5d258f029 100644 --- a/src/Bundle/ChillPersonBundle/config/services.yaml +++ b/src/Bundle/ChillPersonBundle/config/services.yaml @@ -111,3 +111,6 @@ services: Chill\PersonBundle\PersonIdentifier\Normalizer\: resource: '../PersonIdentifier/Normalizer' + + Chill\PersonBundle\PersonIdentifier\Validator\: + resource: '../PersonIdentifier/Validator' diff --git a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml index 691fef833..a20a7d829 100644 --- a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml @@ -73,5 +73,8 @@ relationship: person_creation: If you want to create an household, an address is required: Pour la création d'un ménage, une adresse est requise +person_identifier: + This identifier must be set: Cet identifiant doit être présent. + accompanying_course_work: The endDate should be greater or equal than the start date: La date de fin doit être égale ou supérieure à la date de début