mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-26 16:45:01 +00:00
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.
This commit is contained in:
@@ -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<string, Record<string, unknown>>,
|
||||
> = {
|
||||
[K in Extract<keyof M, string>]: {
|
||||
propertyPath: K;
|
||||
title: string;
|
||||
parameters?: M[K]; // ← uniquement ces clés (pas d’extras)
|
||||
parameters?: M[K];
|
||||
type?: string;
|
||||
};
|
||||
}[Extract<keyof M, string>];
|
||||
@@ -53,18 +52,19 @@ export interface ValidationExceptionInterface<
|
||||
>,
|
||||
> extends Error {
|
||||
name: "ValidationException";
|
||||
/** Copie du payload serveur (utile pour logs/diagnostic) */
|
||||
problem: ValidationProblemFromMap<M>;
|
||||
/** Liste compacte "Titre: chemin" */
|
||||
/** Full server payload copy */
|
||||
problems: ValidationProblemFromMap<M>;
|
||||
/** A list of all violations, with property key */
|
||||
violationsList: ViolationFromMap<M>[];
|
||||
/** Compact list "Title: path" */
|
||||
violations: string[];
|
||||
/** Uniquement les titres */
|
||||
/** Only titles */
|
||||
titles: string[];
|
||||
/** Uniquement les chemins de propriété */
|
||||
/** Only property paths */
|
||||
propertyPaths: Extract<keyof M, string>[];
|
||||
/** Indexation par propriété (utile pour afficher par champ) */
|
||||
/** Indexing by property (useful for display by field) */
|
||||
byProperty: Record<Extract<keyof M, string>, string[]>;
|
||||
}
|
||||
|
||||
export class ValidationException<
|
||||
M extends Record<string, Record<string, unknown>> = Record<
|
||||
string,
|
||||
@@ -75,8 +75,9 @@ export class ValidationException<
|
||||
implements ValidationExceptionInterface<M>
|
||||
{
|
||||
public readonly name = "ValidationException" as const;
|
||||
public readonly problem: ValidationProblemFromMap<M>;
|
||||
public readonly problems: ValidationProblemFromMap<M>;
|
||||
public readonly violations: string[];
|
||||
public readonly violationsList: ViolationFromMap<M>[];
|
||||
public readonly titles: string[];
|
||||
public readonly propertyPaths: Extract<keyof M, string>[];
|
||||
public readonly byProperty: Record<Extract<keyof M, string>, 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}`,
|
||||
);
|
||||
|
@@ -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;
|
||||
|
||||
/**
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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<Person> => {
|
||||
return makeFetch<PersonWrite, Person, WritePersonViolationMap>(
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<div class="form-floating">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('lastName') }"
|
||||
:class="{ 'is-invalid': hasViolation('lastName') }"
|
||||
id="lastname"
|
||||
v-model="lastName"
|
||||
:placeholder="trans(PERSON_MESSAGES_PERSON_LASTNAME)"
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('lastName')"
|
||||
v-for="err in violationTitles('lastName')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="form-floating">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('firstName') }"
|
||||
:class="{ 'is-invalid': hasViolation('firstName') }"
|
||||
id="firstname"
|
||||
v-model="firstName"
|
||||
:placeholder="trans(PERSON_MESSAGES_PERSON_FIRSTNAME)"
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('firstName')"
|
||||
v-for="err in violationTitles('firstName')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -94,6 +94,7 @@
|
||||
<div class="form-floating">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
:class="{'is-invalid': hasViolationWithParameter('identifiers', 'definition_id', worker.definition_id.toString())}"
|
||||
type="text"
|
||||
:name="'worker_' + worker.definition_id"
|
||||
:placeholder="localizeString(worker.label)"
|
||||
@@ -103,6 +104,12 @@
|
||||
localizeString(worker.label)
|
||||
}}</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in violationTitlesWithParameter('identifiers', 'definition_id', worker.definition_id.toString())"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +118,7 @@
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-select form-select-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('gender') }"
|
||||
:class="{ 'is-invalid': hasViolation('gender') }"
|
||||
id="gender"
|
||||
v-model="gender"
|
||||
>
|
||||
@@ -127,7 +134,7 @@
|
||||
}}</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('gender')"
|
||||
v-for="err in violationTitles('gender')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -140,7 +147,7 @@
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-select form-select-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('center') }"
|
||||
:class="{ 'is-invalid': hasViolation('center') }"
|
||||
id="center"
|
||||
v-model="center"
|
||||
>
|
||||
@@ -154,7 +161,7 @@
|
||||
<label for="center">{{ trans(PERSON_MESSAGES_PERSON_CENTER_TITLE) }}</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('center')"
|
||||
v-for="err in violationTitles('center')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -167,7 +174,7 @@
|
||||
<div class="form-floating">
|
||||
<select
|
||||
class="form-select form-select-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('civility') }"
|
||||
:class="{ 'is-invalid': hasViolation('civility') }"
|
||||
id="civility"
|
||||
v-model="civility"
|
||||
>
|
||||
@@ -181,7 +188,7 @@
|
||||
<label for="civility">{{ trans(PERSON_MESSAGES_PERSON_CIVILITY_TITLE) }}</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('civility')"
|
||||
v-for="err in violationTitles('civility')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -197,7 +204,7 @@
|
||||
<div class="form-floating">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('birthdate') }"
|
||||
:class="{ 'is-invalid': hasViolation('birthdate') }"
|
||||
name="birthdate"
|
||||
type="date"
|
||||
v-model="birthDate"
|
||||
@@ -207,7 +214,7 @@
|
||||
<label for="birthdate">{{ trans(BIRTHDATE) }}</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('birthdate')"
|
||||
v-for="err in violationTitles('birthdate')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -223,7 +230,7 @@
|
||||
<div class="form-floating">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('phonenumber') }"
|
||||
:class="{ 'is-invalid': hasViolation('phonenumber') }"
|
||||
v-model="phonenumber"
|
||||
:placeholder="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
|
||||
:aria-label="trans(PERSON_MESSAGES_PERSON_PHONENUMBER)"
|
||||
@@ -232,7 +239,7 @@
|
||||
<label for="phonenumber">{{ trans(PERSON_MESSAGES_PERSON_PHONENUMBER) }}</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('phonenumber')"
|
||||
v-for="err in violationTitles('phonenumber')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -248,7 +255,7 @@
|
||||
<div class="form-floating">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('mobilenumber') }"
|
||||
:class="{ 'is-invalid': hasViolation('mobilenumber') }"
|
||||
v-model="mobilenumber"
|
||||
:placeholder="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
|
||||
:aria-label="trans(PERSON_MESSAGES_PERSON_MOBILENUMBER)"
|
||||
@@ -257,7 +264,7 @@
|
||||
<label for="mobilenumber">{{ trans(PERSON_MESSAGES_PERSON_MOBILENUMBER) }}</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('mobilenumber')"
|
||||
v-for="err in violationTitles('mobilenumber')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -273,7 +280,7 @@
|
||||
<div class="form-floating">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
:class="{ 'is-invalid': hasValidationError('email') }"
|
||||
:class="{ 'is-invalid': hasViolation('email') }"
|
||||
v-model="email"
|
||||
:placeholder="trans(PERSON_MESSAGES_PERSON_EMAIL)"
|
||||
:aria-label="trans(PERSON_MESSAGES_PERSON_EMAIL)"
|
||||
@@ -282,7 +289,7 @@
|
||||
<label for="email">{{ trans(PERSON_MESSAGES_PERSON_EMAIL) }}</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="err in validationError('email')"
|
||||
v-for="err in violationTitles('email')"
|
||||
class="invalid-feedback was-validated-force"
|
||||
>
|
||||
{{ err }}
|
||||
@@ -603,19 +610,49 @@ function addQueryItem(field: "lastName" | "firstName", queryItem: string) {
|
||||
}
|
||||
|
||||
type WritePersonViolationKey = Extract<keyof WritePersonViolationMap, string>;
|
||||
const validationErrors = ref<Partial<Record<WritePersonViolationKey, string[]>>>({});
|
||||
const violationsList = ref<ValidationExceptionInterface<WritePersonViolationMap>["violationsList"]>([]);
|
||||
|
||||
function violationTitles<P extends WritePersonViolationKey>(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<keyof WritePersonViolationMap[P], string>
|
||||
>(
|
||||
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<P extends WritePersonViolationKey>(property: P): boolean {
|
||||
return violationTitles(property).length > 0;
|
||||
}
|
||||
function hasViolationWithParameter<
|
||||
P extends WritePersonViolationKey,
|
||||
Param extends Extract<keyof WritePersonViolationMap[P], string>
|
||||
>(
|
||||
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<void> {
|
||||
emit("onPersonCreated", { person: createdPerson });
|
||||
} catch (e: unknown) {
|
||||
if (isValidationException<WritePersonViolationMap>(e)) {
|
||||
console.log(e.byProperty);
|
||||
validationErrors.value = e.byProperty;
|
||||
violationsList.value = e.violationsList;
|
||||
} else {
|
||||
toast.error(trans(PERSON_EDIT_ERROR_WHILE_SAVING));
|
||||
}
|
||||
|
@@ -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')
|
||||
|
@@ -111,3 +111,6 @@ services:
|
||||
|
||||
Chill\PersonBundle\PersonIdentifier\Normalizer\:
|
||||
resource: '../PersonIdentifier/Normalizer'
|
||||
|
||||
Chill\PersonBundle\PersonIdentifier\Validator\:
|
||||
resource: '../PersonIdentifier/Validator'
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user