mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch '29-person-modal-on-the-fly-add-center' into 'master'
Resolve "La modale "on the fly" pour créer une personne ne fonctionne pas: il manque le centre" Closes #29 See merge request Chill-Projet/chill-bundles!468
This commit is contained in:
commit
f7fa9c31f3
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Security\Authorization;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
class AuthorizationHelperForCurrentUser implements AuthorizationHelperForCurrentUserInterface
|
||||||
|
{
|
||||||
|
private AuthorizationHelperInterface $authorizationHelper;
|
||||||
|
|
||||||
|
private Security $security;
|
||||||
|
|
||||||
|
public function __construct(AuthorizationHelperInterface $authorizationHelper, Security $security)
|
||||||
|
{
|
||||||
|
$this->authorizationHelper = $authorizationHelper;
|
||||||
|
$this->security = $security;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReachableCenters(string $role, ?Scope $scope = null): array
|
||||||
|
{
|
||||||
|
if (!$this->security->getUser() instanceof User) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->authorizationHelper->getReachableCenters($this->security->getUser(), $role, $scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReachableScopes(string $role, $center): array
|
||||||
|
{
|
||||||
|
if (!$this->security->getUser() instanceof User) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->authorizationHelper->getReachableScopes($this->security->getUser(), $role, $center);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Security\Authorization;
|
||||||
|
|
||||||
|
interface AuthorizationHelperForCurrentUserInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get reachable Centers for the given user, role,
|
||||||
|
* and optionnaly Scope.
|
||||||
|
*
|
||||||
|
* @return Center[]
|
||||||
|
*/
|
||||||
|
public function getReachableCenters(string $role, ?Scope $scope = null): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|Center|Center[] $center
|
||||||
|
*/
|
||||||
|
public function getReachableScopes(string $role, $center): array;
|
||||||
|
}
|
@ -31,6 +31,10 @@ class CenterNormalizer implements DenormalizerInterface, NormalizerInterface
|
|||||||
|
|
||||||
public function denormalize($data, $type, $format = null, array $context = [])
|
public function denormalize($data, $type, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
if (null === $data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (false === array_key_exists('type', $data)) {
|
if (false === array_key_exists('type', $data)) {
|
||||||
throw new InvalidArgumentException('missing "type" key in data');
|
throw new InvalidArgumentException('missing "type" key in data');
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,11 @@ services:
|
|||||||
autowire: true
|
autowire: true
|
||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
|
|
||||||
|
Chill\MainBundle\Security\:
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
resource: '../../Security'
|
||||||
|
|
||||||
Chill\MainBundle\Security\Resolver\CenterResolverDispatcher:
|
Chill\MainBundle\Security\Resolver\CenterResolverDispatcher:
|
||||||
arguments:
|
arguments:
|
||||||
- !tagged_iterator chill_main.center_resolver
|
- !tagged_iterator chill_main.center_resolver
|
||||||
|
@ -13,12 +13,13 @@ namespace Chill\PersonBundle\Controller;
|
|||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||||
use Chill\MainBundle\Entity\Address;
|
use Chill\MainBundle\Entity\Address;
|
||||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
|
||||||
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
|
||||||
use Chill\PersonBundle\Entity\Person;
|
use Chill\PersonBundle\Entity\Person;
|
||||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
|
use Chill\PersonBundle\Security\AuthorizedCenterOnPersonCreationInterface;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@ -26,16 +27,37 @@ use function in_array;
|
|||||||
|
|
||||||
class PersonApiController extends ApiController
|
class PersonApiController extends ApiController
|
||||||
{
|
{
|
||||||
private AuthorizationHelper $authorizationHelper;
|
private AuthorizedCenterOnPersonCreationInterface $authorizedCenterOnPersonCreation;
|
||||||
|
|
||||||
private ConfigPersonAltNamesHelper $configPersonAltNameHelper;
|
private ConfigPersonAltNamesHelper $configPersonAltNameHelper;
|
||||||
|
|
||||||
|
private bool $showCenters;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
AuthorizationHelper $authorizationHelper,
|
AuthorizedCenterOnPersonCreationInterface $authorizedCenterOnPersonCreation,
|
||||||
ConfigPersonAltNamesHelper $configPersonAltNameHelper
|
ConfigPersonAltNamesHelper $configPersonAltNameHelper,
|
||||||
|
ParameterBagInterface $parameterBag
|
||||||
) {
|
) {
|
||||||
$this->authorizationHelper = $authorizationHelper;
|
$this->authorizedCenterOnPersonCreation = $authorizedCenterOnPersonCreation;
|
||||||
$this->configPersonAltNameHelper = $configPersonAltNameHelper;
|
$this->configPersonAltNameHelper = $configPersonAltNameHelper;
|
||||||
|
$this->showCenters = $parameterBag->get('chill_main')['acl']['form_show_centers'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route("/api/1.0/person/creation/authorized-centers",
|
||||||
|
* name="chill_person_person_creation_authorized_centers"
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function authorizedCentersForCreation(): Response
|
||||||
|
{
|
||||||
|
$centers = $this->authorizedCenterOnPersonCreation->getCenters();
|
||||||
|
|
||||||
|
return $this->json(
|
||||||
|
['showCenters' => $this->showCenters, 'centers' => $centers],
|
||||||
|
Response::HTTP_OK,
|
||||||
|
[],
|
||||||
|
['gropus' => ['read']]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1567,7 +1567,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
*
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setCenter(Center $center): self
|
public function setCenter(?Center $center): self
|
||||||
{
|
{
|
||||||
$modification = new DateTimeImmutable('now');
|
$modification = new DateTimeImmutable('now');
|
||||||
|
|
||||||
@ -1577,7 +1577,11 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->centerHistory[] = $new = new PersonCenterHistory($this, $center, $modification);
|
if (null === $center) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->centerHistory[] = new PersonCenterHistory($this, $center, $modification);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ import { createApp } from 'vue';
|
|||||||
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
|
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
|
||||||
import { appMessages } from './js/i18n';
|
import { appMessages } from './js/i18n';
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
|
import VueToast from 'vue-toast-notification';
|
||||||
|
import 'vue-toast-notification/dist/theme-sugar.css';
|
||||||
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
@ -12,5 +14,11 @@ const app = createApp({
|
|||||||
})
|
})
|
||||||
.use(store)
|
.use(store)
|
||||||
.use(i18n)
|
.use(i18n)
|
||||||
|
.use(VueToast, {
|
||||||
|
position: "bottom-right",
|
||||||
|
type: "error",
|
||||||
|
duration: 5000,
|
||||||
|
dismissible: true
|
||||||
|
})
|
||||||
.component('app', App)
|
.component('app', App)
|
||||||
.mount('#household_members_editor');
|
.mount('#household_members_editor');
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* GET a person by id
|
* GET a person by id
|
||||||
*/
|
*/
|
||||||
@ -22,6 +24,8 @@ const getCivilities = () =>
|
|||||||
throw Error('Error with request resource response');
|
throw Error('Error with request resource response');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getCentersForPersonCreation = () => makeFetch('GET', '/api/1.0/person/creation/authorized-centers', null);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* POST a new person
|
* POST a new person
|
||||||
*/
|
*/
|
||||||
@ -59,6 +63,7 @@ const patchPerson = (id, body) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
getCentersForPersonCreation,
|
||||||
getPerson,
|
getPerson,
|
||||||
getPersonAltNames,
|
getPersonAltNames,
|
||||||
getCivilities,
|
getCivilities,
|
||||||
|
@ -87,6 +87,18 @@
|
|||||||
<label>{{ $t('person.gender.title') }}</label>
|
<label>{{ $t('person.gender.title') }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-floating mb-3" v-if="showCenters && config.centers.length > 1">
|
||||||
|
<select
|
||||||
|
class="form-select form-select-lg"
|
||||||
|
id="center"
|
||||||
|
v-model="center"
|
||||||
|
>
|
||||||
|
<option selected disabled>{{ $t('person.center.placeholder') }}</option>
|
||||||
|
<option v-for="c in config.centers" :value="c" :key="c.id" >{{ c.name }}</option>
|
||||||
|
</select>
|
||||||
|
<label>{{ $t('person.center.title') }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-floating mb-3">
|
<div class="form-floating mb-3">
|
||||||
<select
|
<select
|
||||||
class="form-select form-select-lg"
|
class="form-select form-select-lg"
|
||||||
@ -166,7 +178,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getCivilities, getPerson, getPersonAltNames } from '../../_api/OnTheFly';
|
import { getCentersForPersonCreation, getCivilities, getPerson, getPersonAltNames } from '../../_api/OnTheFly';
|
||||||
import PersonRenderBox from '../Entity/PersonRenderBox.vue';
|
import PersonRenderBox from '../Entity/PersonRenderBox.vue';
|
||||||
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
|
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
|
||||||
|
|
||||||
@ -182,13 +194,18 @@ export default {
|
|||||||
return {
|
return {
|
||||||
person: {
|
person: {
|
||||||
type: 'person',
|
type: 'person',
|
||||||
|
lastName: '',
|
||||||
|
firstName: '',
|
||||||
altNames: [],
|
altNames: [],
|
||||||
addressId: null
|
addressId: null,
|
||||||
|
center: null,
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
altNames: [],
|
altNames: [],
|
||||||
civilities: []
|
civilities: [],
|
||||||
|
centers: [],
|
||||||
},
|
},
|
||||||
|
showCenters: false, // NOTE: must remains false if the form is not in create mode
|
||||||
showAddressFormValue: false,
|
showAddressFormValue: false,
|
||||||
addAddress: {
|
addAddress: {
|
||||||
options: {
|
options: {
|
||||||
@ -254,6 +271,19 @@ export default {
|
|||||||
set(value) { this.showAddressFormValue = value; },
|
set(value) { this.showAddressFormValue = value; },
|
||||||
get() { return this.showAddressFormValue; }
|
get() { return this.showAddressFormValue; }
|
||||||
},
|
},
|
||||||
|
center: {
|
||||||
|
set(value) {
|
||||||
|
console.log('will set center', value);
|
||||||
|
this.person.center = {id: value.id, type: value.type};
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
const center = this.config.centers.find(c => this.person.center !== null && this.person.center.id === c.id);
|
||||||
|
|
||||||
|
console.log('center get', center);
|
||||||
|
|
||||||
|
return typeof center === 'undefined' ? null : center;
|
||||||
|
},
|
||||||
|
},
|
||||||
genderClass() {
|
genderClass() {
|
||||||
switch (this.person.gender) {
|
switch (this.person.gender) {
|
||||||
case 'woman':
|
case 'woman':
|
||||||
@ -295,23 +325,35 @@ export default {
|
|||||||
this.config.civilities = civilities.results;
|
this.config.civilities = civilities.results;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.action !== 'create') {
|
if (this.action !== 'create') {
|
||||||
this.loadData();
|
this.loadData();
|
||||||
|
} else {
|
||||||
|
getCentersForPersonCreation()
|
||||||
|
.then(params => {
|
||||||
|
this.config.centers = params.centers;
|
||||||
|
this.showCenters = params.showCenters;
|
||||||
|
|
||||||
|
if (this.showCenters && this.config.centers.length === 1) {
|
||||||
|
this.person.center = this.config.centers[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
checkErrors(e) {
|
checkErrors(e) {
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
if (!this.person.lastName) {
|
if (this.person.lastName === "") {
|
||||||
this.errors.push("Le nom ne doit pas être vide.");
|
this.errors.push("Le nom ne doit pas être vide.");
|
||||||
}
|
}
|
||||||
if (!this.person.firstName) {
|
if (this.person.firstName === "") {
|
||||||
this.errors.push("Le prénom ne doit pas être vide.");
|
this.errors.push("Le prénom ne doit pas être vide.");
|
||||||
}
|
}
|
||||||
if (!this.person.gender) {
|
if (!this.person.gender) {
|
||||||
this.errors.push("Le genre doit être renseigné");
|
this.errors.push("Le genre doit être renseigné");
|
||||||
}
|
}
|
||||||
|
if (this.showCenters && this.person.center === null) {
|
||||||
|
this.errors.push("Le centre doit être renseigné");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
loadData() {
|
loadData() {
|
||||||
getPerson(this.id)
|
getPerson(this.id)
|
||||||
|
@ -47,6 +47,10 @@ const personMessages = {
|
|||||||
create_address: "Ajouter une adresse",
|
create_address: "Ajouter une adresse",
|
||||||
show_address_form: "Ajouter une adresse pour un usager non suivi et seul dans un ménage",
|
show_address_form: "Ajouter une adresse pour un usager non suivi et seul dans un ménage",
|
||||||
warning: "Un nouveau ménage va être créé. L'usager sera membre de ce ménage."
|
warning: "Un nouveau ménage va être créé. L'usager sera membre de ce ménage."
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
placeholder: "Choisissez un centre",
|
||||||
|
title: "Centre",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error_only_one_person: "Une seule personne peut être sélectionnée !"
|
error_only_one_person: "Une seule personne peut être sélectionnée !"
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\PersonBundle\Security;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Security\Authorization\AuthorizationHelperForCurrentUserInterface;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
|
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||||
|
|
||||||
|
class AuthorizedCenterOnPersonCreation implements AuthorizedCenterOnPersonCreationInterface
|
||||||
|
{
|
||||||
|
private AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser;
|
||||||
|
|
||||||
|
private bool $showCenters;
|
||||||
|
|
||||||
|
public function __construct(AuthorizationHelperForCurrentUserInterface $authorizationHelperForCurrentUser, ParameterBagInterface $parameterBag)
|
||||||
|
{
|
||||||
|
$this->authorizationHelperForCurrentUser = $authorizationHelperForCurrentUser;
|
||||||
|
$this->showCenter = $parameterBag->get('chill_main')['acl']['form_show_centers'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCenters(): array
|
||||||
|
{
|
||||||
|
if (!$this->showCenter) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->authorizationHelperForCurrentUser->getReachableCenters(PersonVoter::CREATE);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\PersonBundle\Security;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Center;
|
||||||
|
|
||||||
|
interface AuthorizedCenterOnPersonCreationInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array|Center[]
|
||||||
|
*/
|
||||||
|
public function getCenters(): array;
|
||||||
|
}
|
@ -1937,4 +1937,14 @@ paths:
|
|||||||
summary: Return a list of possible altNames that are defined in the config
|
summary: Return a list of possible altNames that are defined in the config
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "OK"
|
description: "OK"
|
||||||
|
|
||||||
|
/1.0/person/creation/authorized-centers:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- person
|
||||||
|
- permissions
|
||||||
|
summary: Return a list of possible centers for person creation
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: "OK"
|
||||||
|
@ -47,9 +47,8 @@ services:
|
|||||||
tags: ['controller.service_arguments']
|
tags: ['controller.service_arguments']
|
||||||
|
|
||||||
Chill\PersonBundle\Controller\PersonApiController:
|
Chill\PersonBundle\Controller\PersonApiController:
|
||||||
arguments:
|
autoconfigure: true
|
||||||
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
|
autowire: true
|
||||||
$configPersonAltNameHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper'
|
|
||||||
tags: ['controller.service_arguments']
|
tags: ['controller.service_arguments']
|
||||||
|
|
||||||
Chill\PersonBundle\Controller\AccompanyingCourseWorkApiController:
|
Chill\PersonBundle\Controller\AccompanyingCourseWorkApiController:
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
services:
|
services:
|
||||||
|
|
||||||
|
Chill\PersonBundle\Security\:
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
resource: '../../Security'
|
||||||
|
|
||||||
chill.person.security.authorization.person:
|
chill.person.security.authorization.person:
|
||||||
autowire: true
|
autowire: true
|
||||||
autoconfigure: true
|
autoconfigure: true
|
||||||
|
@ -22,6 +22,8 @@ The lastname cannot be empty: Le nom de famille ne peut pas être vide
|
|||||||
The gender must be set: Le genre doit être renseigné
|
The gender must be set: Le genre doit être renseigné
|
||||||
You are not allowed to perform this action: Vous n'avez pas le droit de changer cette valeur.
|
You are not allowed to perform this action: Vous n'avez pas le droit de changer cette valeur.
|
||||||
|
|
||||||
|
A center is required: Un centre est requis
|
||||||
|
|
||||||
#export list
|
#export list
|
||||||
You must select at least one element: Vous devez sélectionner au moins un élément
|
You must select at least one element: Vous devez sélectionner au moins un élément
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user