diff --git a/CHANGELOG.md b/CHANGELOG.md
index 568224874..233c1f51b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,8 @@ and this project adheres to
* [main] Add mainLocation field to User entity and add it in user form type
* [course list in person context] show full username/label for ref
* [accompanying period work] remove the possibility to generate document from an accompanying period work
+* vuejs: add validation on required fields for AddPerson, Address and Location components
+* vuejs: treat 422 validation errors in locations and AddPerson components
## Test releases
diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/App.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/App.vue
index 2fb9d022d..4809a5fae 100644
--- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/App.vue
+++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/App.vue
@@ -11,7 +11,7 @@ import Location from './components/Location.vue';
export default {
name: "App",
- props: ['hasSocialIssues', 'hasLocation', 'hasPerson'],
+ props: ['hasSocialIssues', 'hasLocation', 'hasPerson'],
components: {
ConcernedGroups,
SocialIssuesAcc,
diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location.vue
index b8249ccc5..91c5db839 100644
--- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location.vue
+++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location.vue
@@ -24,7 +24,7 @@
v-model="location"
>
-
+
diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue
index 35bf9a065..c9a1c233c 100644
--- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue
+++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/components/Location/NewLocation.vue
@@ -18,15 +18,6 @@
@@ -81,7 +78,8 @@
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import { mapState } from "vuex";
-import { getLocationTypes, postLocation } from "../../api";
+import { getLocationTypes } from "../../api";
+import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
export default {
name: "NewLocation",
@@ -89,7 +87,7 @@ export default {
Modal,
AddAddress,
},
- props: ['locations'],
+ props: ['availableLocations'],
data() {
return {
errors: [],
@@ -223,7 +221,6 @@ export default {
},
saveNewLocation() {
if (this.checkForm()) {
- console.log('saveNewLocation', this.selected);
let body = {
type: 'location',
name: this.selected.name,
@@ -242,23 +239,28 @@ export default {
}
});
}
- postLocation(body)
- .then(
- location => new Promise(resolve => {
- this.locations.push(location);
- this.$store.dispatch('updateLocation', location);
- resolve();
- this.modal.showModal = false;
- })
- ).catch(
- err => {
- this.errors.push(err.message);
+
+ makeFetch('POST', '/api/1.0/main/location.json', body)
+ .then(response => {
+ this.$store.dispatch('addAvailableLocationGroup', {
+ locationGroup: 'Localisations nouvellement créées',
+ locations: [response]
+ });
+ this.$store.dispatch('updateLocation', response);
+ this.modal.showModal = false;
+ })
+ .catch((error) => {
+ if (error.name === 'ValidationException') {
+ for (let v of error.violations) {
+ this.errors.push(v);
+ }
+ } else {
+ this.errors.push('An error occurred');
}
- );
+ })
};
},
submitNewAddress(payload) {
- console.log('submitNewAddress', payload);
this.selected.addressId = payload.addressId;
this.addAddress.context.addressId = payload.addressId;
this.addAddress.context.edit = true;
diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/index.js b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/index.js
index 66657df47..b7a5c791b 100644
--- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/index.js
+++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/index.js
@@ -12,7 +12,11 @@ const hasLocation = document.querySelector('#location') !== null;
const hasPerson = document.querySelector('#add-persons') !== null;
const app = createApp({
- template: ` `,
+ template: ` `,
data() {
return {
hasSocialIssues,
diff --git a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js
index 4bf70b006..f94381fc2 100644
--- a/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js
+++ b/src/Bundle/ChillActivityBundle/Resources/public/vuejs/Activity/store.js
@@ -240,6 +240,9 @@ const store = createStore({
});
commit("updateActionsSelected", payload);
},
+ addAvailableLocationGroup({ commit }, payload) {
+ commit("addAvailableLocationGroup", payload);
+ },
addPersonsInvolved({ commit }, payload) {
//console.log('### action addPersonsInvolved', payload.result.type);
switch (payload.result.type) {
diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss
index a570b645a..03eb7f281 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss
+++ b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss
@@ -418,3 +418,8 @@ span.item-key {
background-color: #0000000a;
//text-decoration: dotted underline;
}
+
+// increase toast message z-index (above all modals)
+div.v-toast {
+ z-index: 10000!important;
+}
\ No newline at end of file
diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.js b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.js
index 96a95ad93..5ad119858 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.js
+++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.js
@@ -85,7 +85,9 @@ const fetchScopes = () => {
const ValidationException = (response) => {
const error = {};
error.name = 'ValidationException';
- error.violations = response.violations.map((violation) => `${violation.title}`);
+ error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
+ error.titles = response.violations.map((violation) => violation.title);
+ error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
return error;
}
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue
index ecc7b7e38..1f62269de 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue
@@ -98,6 +98,8 @@
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
+ v-bind:errors="this.errors"
+ v-bind:checkErrors="this.checkErrors"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
@@ -123,6 +125,8 @@
v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity"
v-bind:flag="this.flag"
+ v-bind:errors="this.errors"
+ v-bind:checkErrors="this.checkErrors"
v-bind:insideModal="false"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
@@ -256,8 +260,10 @@ export default {
editPane: false,
datePane: false,
loading: false,
- success: false
+ success: false,
+ dirty: false
},
+ errors: [],
defaultz: {
button: {
text: { create: 'add_an_address_title', edit: 'edit_address' },
@@ -529,6 +535,23 @@ export default {
});
},
+ checkErrors() {
+ this.errors = [];
+ if (this.flag.dirty) {
+ if (this.entity.selected.country === null) {
+ this.errors.push("Un pays doit être sélectionné.");
+ }
+ if (Object.keys(this.entity.selected.city).length === 0) {
+ this.errors.push("Une ville doit être sélectionnée.");
+ }
+ if (!this.entity.selected.isNoAddress) {
+ if (this.entity.selected.address.street === null || this.entity.selected.address.streetNumber === null) {
+ this.errors.push("Une adresse doit être sélectionnée.");
+ }
+ }
+ }
+ },
+
/*
* Make form ready for new changes
*/
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue
index 6d9c32524..12e45d0b7 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue
@@ -12,6 +12,7 @@
@search-change="listenInputSearch"
ref="addressSelector"
@select="selectAddress"
+ @remove="remove"
name="field"
track-by="id"
label="value"
@@ -56,7 +57,7 @@ import { searchReferenceAddresses, fetchReferenceAddresses } from '../../api.js'
export default {
name: 'AddressSelection',
components: { VueMultiselect },
- props: ['entity', 'context', 'updateMapCenter'],
+ props: ['entity', 'context', 'updateMapCenter', 'flag', 'checkErrors'],
data() {
return {
value: this.context.edit ? this.entity.address.addressReference : null,
@@ -109,6 +110,13 @@ export default {
this.entity.selected.address.streetNumber = value.streetNumber;
this.entity.selected.writeNew.address = false;
this.updateMapCenter(value.point);
+ this.flag.dirty = true;
+ this.checkErrors();
+ },
+ remove() {
+ this.flag.dirty = true;
+ this.entity.selected.address = {};
+ this.checkErrors();
},
listenInputSearch(query) {
//console.log('listenInputSearch', query, this.isAddressSelectorOpen);
@@ -149,6 +157,8 @@ export default {
this.entity.selected.address.street = addr.street;
this.entity.selected.address.streetNumber = addr.number;
this.entity.selected.writeNew.address = true;
+ this.flag.dirty = true;
+ this.checkErrors();
}
},
splitAddress(address) {
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue
index 5191b3a31..4883957bb 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CitySelection.vue
@@ -7,6 +7,7 @@
@search-change="listenInputSearch"
ref="citySelector"
@select="selectCity"
+ @remove="remove"
name="field"
track-by="id"
label="value"
@@ -55,12 +56,12 @@ import { searchCities, fetchCities } from '../../api.js';
export default {
name: 'CitySelection',
components: { VueMultiselect },
- props: ['entity', 'context', 'focusOnAddress', 'updateMapCenter'],
+ props: ['entity', 'context', 'focusOnAddress', 'updateMapCenter', 'flag', 'checkErrors'],
emits: ['getReferenceAddresses'],
data() {
return {
value: this.context.edit ? this.entity.address.postcode : null,
- isLoading: false
+ isLoading: false,
}
},
computed: {
@@ -123,6 +124,13 @@ export default {
if (value.center) {
this.updateMapCenter(value.center);
}
+ this.flag.dirty = true;
+ this.checkErrors();
+ },
+ remove() {
+ this.flag.dirty = true;
+ this.entity.selected.city = {};
+ this.checkErrors();
},
listenInputSearch(query) {
if (query.length > 2) {
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue
index c1b1d1b55..09ba8421d 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/CountrySelection.vue
@@ -12,7 +12,9 @@
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
- @select="selectCountry">
+ @select="selectCountry"
+ @remove="remove"
+ >
@@ -23,7 +25,7 @@ import VueMultiselect from 'vue-multiselect';
export default {
name: 'CountrySelection',
components: { VueMultiselect },
- props: ['context', 'entity'],
+ props: ['context', 'entity', 'flag', 'checkErrors'],
emits: ['getCities'],
data() {
return {
@@ -34,14 +36,13 @@ export default {
},
computed: {
sortedCountries() {
- //console.log('sorted countries');
const countries = this.entity.loaded.countries;
let sortedCountries = [];
sortedCountries.push(...countries.filter(c => c.countryCode === 'FR'))
sortedCountries.push(...countries.filter(c => c.countryCode === 'BE'))
sortedCountries.push(...countries.filter(c => c.countryCode !== 'FR').filter(c => c.countryCode !== 'BE'))
return sortedCountries;
- }
+ },
},
mounted() {
this.init();
@@ -50,6 +51,7 @@ export default {
init() {
if (this.value !== undefined) {
this.selectCountry(this.value);
+ this.flag.dirty = false;
}
},
selectCountryByCode(countryCode) {
@@ -62,7 +64,13 @@ export default {
//console.log('select country', value);
this.entity.selected.country = value;
this.$emit('getCities', value);
- }
+ this.checkErrors();
+ },
+ remove() {
+ this.flag.dirty = true;
+ this.entity.selected.country = null;
+ this.checkErrors();
+ },
}
};
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue
index af1263b43..486d28e73 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/EditPane.vue
@@ -7,6 +7,12 @@
Loading...
+
+
{{ $t('select_an_address_title') }}
@@ -25,6 +31,8 @@
@@ -33,13 +41,17 @@
v-bind:context="context"
v-bind:focusOnAddress="focusOnAddress"
v-bind:updateMapCenter="updateMapCenter"
+ v-bind:flag="flag"
+ v-bind:checkErrors="checkErrors"
@getReferenceAddresses="$emit('getReferenceAddresses', selected.city)">
+ v-bind:updateMapCenter="updateMapCenter"
+ v-bind:flag="flag"
+ v-bind:checkErrors="checkErrors">
@@ -99,7 +111,9 @@ export default {
'flag',
'entity',
'errorMsg',
- 'insideModal'
+ 'insideModal',
+ 'errors',
+ 'checkErrors',
],
emits: ['getCities', 'getReferenceAddresses'],
data() {
@@ -128,7 +142,7 @@ export default {
get() {
return this.entity.selected.isNoAddress;
}
- }
+ },
},
methods: {
focusOnAddress() {
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/components/OnTheFly.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/components/OnTheFly.vue
index d4342fdee..8c93eec71 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/components/OnTheFly.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/components/OnTheFly.vue
@@ -90,7 +90,7 @@ export default {
OnTheFlyThirdparty,
OnTheFlyCreate
},
- props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'parent'],
+ props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'parent', 'canCloseModal'],
emits: ['saveFormOnTheFly'],
data() {
return {
@@ -162,7 +162,20 @@ export default {
return 'entity-' + this.type + ' badge-' + this.type;
}
},
+ watch: {
+ canCloseModal: {
+ handler: function(val, oldVal) {
+ if (val) {
+ this.closeModal();
+ }
+ },
+ deep: true
+ }
+ },
methods: {
+ closeModal() {
+ this.modal.showModal = false;
+ },
openModal() {
//console.log('## OPEN ON THE FLY MODAL');
//console.log('## type:', this.type, ', action:', this.action);
@@ -200,8 +213,6 @@ export default {
// pass datas to parent
this.$emit('saveFormOnTheFly', { type: type, data: data });
-
- this.modal.showModal = false;
},
buildLocation(id, type) {
if (type === 'person') {
diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php
index 73b83f246..91e430a2f 100644
--- a/src/Bundle/ChillPersonBundle/Entity/Person.php
+++ b/src/Bundle/ChillPersonBundle/Entity/Person.php
@@ -151,7 +151,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var DateTime
*
* @ORM\Column(type="date", nullable=true)
- * @Assert\Date
* @Birthdate
*/
private $birthdate;
@@ -259,7 +258,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=255)
- * @Assert\NotBlank
+ * @Assert\NotBlank(message="The firstname cannot be empty")
* @Assert\Length(
* max=255,
* )
@@ -282,7 +281,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=9, nullable=true)
- * @Assert\NotNull
+ * @Assert\NotNull(message="The gender must be set")
*/
private $gender;
@@ -326,7 +325,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=255)
- * @Assert\NotBlank
+ * @Assert\NotBlank(message="The lastname cannot be empty")
* @Assert\Length(
* max=255,
* )
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
index f118671ed..59647e160 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue
@@ -66,9 +66,10 @@
+ @saveFormOnTheFly="saveFormOnTheFly"
+ :canCloseModal="canCloseOnTheFlyModal">
@@ -91,8 +92,7 @@ import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import PersonSuggestion from './AddPersons/PersonSuggestion';
import { searchEntities } from 'ChillPersonAssets/vuejs/_api/AddPersons';
-import { postPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly";
-import { postThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly";
+import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
export default {
name: 'AddPersons',
@@ -120,7 +120,8 @@ export default {
suggested: [],
selected: [],
priorSuggestion: {}
- }
+ },
+ canCloseOnTheFlyModal: false
}
},
computed: {
@@ -267,22 +268,36 @@ export default {
saveFormOnTheFly({ type, data }) {
console.log('saveFormOnTheFly from addPersons, type', type, ', data', data);
if (type === 'person') {
- console.log('type person with', data);
- postPerson(data)
- .then(person => new Promise((resolve, reject) => {
- console.log('onthefly create: post person', person);
- this.newPriorSuggestion(person);
- resolve();
- }));
+ makeFetch('POST', '/api/1.0/person/person.json', data)
+ .then(response => {
+ this.newPriorSuggestion(response);
+ this.canCloseOnTheFlyModal = true;
+ })
+ .catch((error) => {
+ if (error.name === 'ValidationException') {
+ for (let v of error.violations) {
+ this.$toast.open({message: v });
+ }
+ } else {
+ this.$toast.open({message: 'An error occurred'});
+ }
+ })
}
else if (type === 'thirdparty') {
- console.log('type thirdparty with', data);
- postThirdparty(data)
- .then(thirdparty => new Promise((resolve, reject) => {
- console.log('onthefly create: post thirdparty', thirdparty);
- this.newPriorSuggestion(thirdparty);
- resolve();
- }));
+ makeFetch('POST', '/api/1.0/thirdparty/thirdparty.json', data)
+ .then(response => {
+ this.newPriorSuggestion(response);
+ this.canCloseOnTheFlyModal = true;
+ })
+ .catch((error) => {
+ if (error.name === 'ValidationException') {
+ for (let v of error.violations) {
+ this.$toast.open({message: v });
+ }
+ } else {
+ this.$toast.open({message: 'An error occurred'});
+ }
+ })
}
}
},
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue
index 799e5651b..9543da3bb 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue
@@ -22,24 +22,45 @@
@@ -80,11 +101,17 @@
+
+
@@ -108,6 +135,7 @@ export default {
config: {
altNames: []
},
+ errors: []
}
},
computed: {
@@ -183,6 +211,18 @@ export default {
}
},
methods: {
+ checkErrors(e) {
+ this.errors = [];
+ if (!this.person.lastName) {
+ this.errors.push("Le nom ne doit pas être vide.");
+ }
+ if (!this.person.firstName) {
+ this.errors.push("Le prénom ne doit pas être vide.");
+ }
+ if (!this.person.gender) {
+ this.errors.push("Le genre doit être renseigné");
+ }
+ },
loadData() {
getPerson(this.id)
.then(person => new Promise((resolve, reject) => {
diff --git a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
index b421e9f43..96cfd565f 100644
--- a/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
+++ b/src/Bundle/ChillPersonBundle/translations/validators.fr.yml
@@ -16,6 +16,9 @@ The birthdate must be before %date%: La date de naissance doit être avant le %d
'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33623456789': 'Numéro de téléphone invalide: il doit commencer par le préfixe international précédé de "+", ne comporter que des chiffres et faire moins de 20 caractères. Ex: +33623456789'
'The email is not valid': 'Le courriel n''est pas valide'
Two addresses has the same validFrom date: La date de validité est identique à celle d'une autre adresse
+The firstname cannot be empty: Le prénom ne peut pas être vide
+The lastname cannot be empty: Le nom de famille ne peut pas être vide
+The gender must be set: Le genre doit être renseigné
#export list
You must select at least one element: Vous devez sélectionner au moins un élément