mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-11-12 15:18:22 +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