handle types and categories in a single select input

This commit is contained in:
Julien Fastré 2021-10-12 15:52:06 +02:00
parent 9eec15873e
commit 88d073b9a9
10 changed files with 323 additions and 78 deletions

View File

@ -2,6 +2,7 @@
namespace Chill\ThirdPartyBundle;
use Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeProviderInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Chill\ThirdPartyBundle\DependencyInjection\CompilerPass\ThirdPartyTypeCompilerPass;
@ -10,6 +11,8 @@ class ChillThirdPartyBundle extends Bundle
public function build(\Symfony\Component\DependencyInjection\ContainerBuilder $container)
{
parent::build($container);
$container->registerForAutoconfiguration(ThirdPartyTypeProviderInterface::class)
->addTag('chill_3party.provider');
$container->addCompilerPass(new ThirdPartyTypeCompilerPass());
}

View File

@ -127,6 +127,8 @@ final class ThirdPartyController extends CRUDController
return $this->getFilterOrderHelperFactory()
->create(self::class)
->addSearchBox(['name', 'company_name', 'acronym'])
//->addToggle('only-active', [])
// ->addOrderBy()
->build();
}
}

View File

@ -368,7 +368,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
public function setTypes(array $type = null)
{
// remove all keys from the input data
$this->type = \array_values($type);
$this->types = \array_values($type);
foreach ($this->children as $child) {
$child->setTypes($type);
@ -387,6 +387,40 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
return $this->types;
}
public function addType(?string $type): self
{
if (NULL === $type) {
return $this;
}
if (!\in_array($type, $this->types ?? [])) {
$this->types[] = $type;
}
foreach ($this->children as $child) {
$child->addType($type);
}
return $this;
}
public function removeType(?string $type): self
{
if (NULL === $type) {
return $this;
}
if (\in_array($type, $this->types ?? [])) {
$this->types = \array_filter($this->types, fn($e) => !\in_array($e, $this->types));
}
foreach ($this->children as $child) {
$child->removeType($type);
}
return $this;
}
/**
* @return bool
*/
@ -541,7 +575,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
}
foreach ($this->children as $child) {
$child->addCategory($child);
$child->addCategory($category);
}
return $this;
@ -556,7 +590,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
$this->categories->removeElement($category);
foreach ($this->children as $child) {
$child->removeCategory($child);
$child->removeCategory($category);
}
return $this;
@ -631,6 +665,72 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
public function addTypesAndCategories($typeAndCategory): self
{
if ($typeAndCategory instanceof ThirdPartyCategory) {
$this->addCategory($typeAndCategory);
return $this;
}
if (is_string($typeAndCategory)) {
$this->addType($typeAndCategory);
return $this;
}
throw new \UnexpectedValueException(sprintf(
"typeAndCategory should be a string or a %s", ThirdPartyCategory::class));
}
public function removeTypesAndCategories($typeAndCategory): self
{
if ($typeAndCategory instanceof ThirdPartyCategory) {
$this->removeCategory($typeAndCategory);
return $this;
}
if (is_string($typeAndCategory)) {
$this->removeType($typeAndCategory);
return $this;
}
throw new \UnexpectedValueException(sprintf(
"typeAndCategory should be a string or a %s", ThirdPartyCategory::class));
}
public function getTypesAndCategories(): array
{
return \array_merge(
$this->getCategories()->toArray(),
$this->getTypes() ?? []
);
}
public function setTypesAndCategories(array $typesAndCategories): self
{
$types = \array_filter($typesAndCategories, fn($item) => !$item instanceof ThirdPartyCategory);
$this->setTypes($types);
// handle categories
foreach ($typesAndCategories as $t) {
$this->addTypesAndCategories($t);
}
$categories = \array_filter($typesAndCategories, fn($item) => $item instanceof ThirdPartyCategory);
$categoriesHashes = \array_map(fn(ThirdPartyCategory $c) => \spl_object_hash($c), $categories);
foreach ($categories as $c) {
$this->addCategory($c);
}
foreach ($this->getCategories() as $t) {
if (!\in_array(\spl_object_hash($t), $categoriesHashes)) {
$this->removeCategory($t);
}
}
return $this;
}
/**
* @param ThirdParty $child

View File

@ -11,9 +11,12 @@ use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use Chill\ThirdPartyBundle\Entity\ThirdPartyProfession;
use Chill\ThirdPartyBundle\Form\Type\PickThirdPartyType;
use Chill\ThirdPartyBundle\Form\Type\PickThirdPartyTypeCategoryType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@ -40,14 +43,14 @@ class ThirdPartyType extends AbstractType
protected TranslatableStringHelper $translatableStringHelper;
protected ObjectManager $om;
protected EntityManagerInterface $om;
public function __construct(
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
ThirdPartyTypeManager $typesManager,
TranslatableStringHelper $translatableStringHelper,
ObjectManager $om
EntityManagerInterface $om
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;
@ -171,21 +174,9 @@ class ThirdPartyType extends AbstractType
}
if (ThirdParty::KIND_CHILD !== $options['kind']) {
$builder
->add('categories', EntityType::class, [
'label' => 'thirdparty.Categories',
'class' => ThirdPartyCategory::class,
'choice_label' => function (ThirdPartyCategory $category): string {
return $this->translatableStringHelper->localize($category->getName());
},
'query_builder' => function (EntityRepository $er): QueryBuilder {
return $er->createQueryBuilder('c')
->where('c.active = true');
},
'required' => true,
'multiple' => true,
'attr' => ['class' => 'select2']
->add('typesAndCategories', PickThirdPartyTypeCategoryType::class, [
'label' => 'thirdparty.Categories'
])
->add('active', ChoiceType::class, [
'label' => 'thirdparty.Status',
@ -196,42 +187,6 @@ class ThirdPartyType extends AbstractType
'expanded' => true,
'multiple' => false
]);
// add the types
$types = [];
foreach ($this->typesManager->getProviders() as $key => $provider) {
$types['chill_3party.key_label.'.$key] = $key;
}
if (count($types) === 1) {
$builder
->add('types', HiddenType::class, [
'data' => array_values($types)
])
->get('types')
->addModelTransformer(new CallbackTransformer(
function (?array $typeArray): ?string {
if (null === $typeArray) {
return null;
}
return implode(',', $typeArray);
},
function (?string $typeStr): ?array {
if (null === $typeStr) {
return null;
}
return explode(',', $typeStr);
}
))
;
} else {
$builder
->add('types', ChoiceType::class, [
'choices' => $types,
'expanded' => true,
'multiple' => true,
'label' => 'thirdparty.Type'
]);
}
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace Chill\ThirdPartyBundle\Form\Type;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use Chill\ThirdPartyBundle\Repository\ThirdPartyCategoryRepository;
use Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class PickThirdPartyTypeCategoryType extends \Symfony\Component\Form\AbstractType
{
private ThirdPartyCategoryRepository $thirdPartyCategoryRepository;
private ThirdPartyTypeManager $thirdPartyTypeManager;
private TranslatableStringHelper $translatableStringHelper;
private TranslatorInterface $translator;
private const PREFIX_TYPE = 'chill_3party.key_label.';
public function __construct(
ThirdPartyCategoryRepository $thirdPartyCategoryRepository,
ThirdPartyTypeManager $thirdPartyTypeManager,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator
) {
$this->thirdPartyCategoryRepository = $thirdPartyCategoryRepository;
$this->thirdPartyTypeManager = $thirdPartyTypeManager;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
}
public function getParent()
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver)
{
$choices = \array_merge(
$this->thirdPartyCategoryRepository->findBy(['active' => true]),
$this->thirdPartyTypeManager->getTypes()
);
\uasort($choices, function ($itemA, $itemB) {
$strA = $itemA instanceof ThirdPartyCategory ? $this->translatableStringHelper
->localize($itemA->getName()) : $this->translator->trans(self::PREFIX_TYPE.$itemA);
$strB = $itemB instanceof ThirdPartyCategory ? $this->translatableStringHelper
->localize($itemB->getName()) : $this->translator->trans(self::PREFIX_TYPE.$itemB);
return $strA <=> $strB;
});
$resolver->setDefaults([
'choices' => $choices,
'attr' => [ 'class' => 'select2' ],
'multiple' => true,
'choice_label' => function($item) {
if ($item instanceof ThirdPartyCategory) {
return $this->translatableStringHelper->localize($item->getName());
}
return self::PREFIX_TYPE.$item;
},
'choice_value' => function($item) {
return $this->reverseTransform($item);
}
]);
}
public function reverseTransform($value)
{
if ($value === null) {
return null;
}
if (is_array($value)){
$r = [];
foreach ($value as $v) {
$r[] = $this->transform($v);
}
return $r;
}
if ($value instanceof ThirdPartyCategory) {
return 'category:'.$value->getId();
}
if (is_string($value)) {
return 'type:'.$value;
}
throw new UnexpectedTypeException($value, \implode(' or ', ['array', 'string', ThirdPartyCategory::class]));
}
}

View File

@ -14,8 +14,7 @@
{{ form_row(form.profession) }}
{% endif %}
{{ form_row(form.types) }}
{{ form_row(form.categories) }}
{{ form_row(form.typesAndCategories) }}
{{ form_row(form.telephone) }}
{{ form_row(form.email) }}

View File

@ -48,13 +48,20 @@
</dd>
{% endif %}
<dt>{{ 'Type'|trans }}</dt>
<dt>{{ 'thirdparty.Categories'|trans }}</dt>
{% set types = [] %}
{% for t in thirdParty.types %}
{% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %}
{% endfor %}
{% for c in thirdParty.categories %}
{% set types = types|merge([ c.name|localize_translatable_string ]) %}
{% endfor %}
<dd>
{{ types|join(', ') }}
{% if types|length > 0 %}
{{ types|join(', ') }}
{% else %}
<p class="chill-no-data-statement">{{ 'thirdParty.Any categories' }}</p>
{% endif %}
</dd>
<dt>{{ 'Phonenumber'|trans }}</dt>

View File

@ -0,0 +1,92 @@
<?php
namespace Chill\ThirdParty\Tests\Entity;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use PHPUnit\Framework\TestCase;
class ThirdPartyTest extends TestCase
{
public function testAddingRemovingActivityTypes()
{
$tp = new ThirdParty();
$cat1 = new ThirdPartyCategory();
$cat2 = new ThirdPartyCategory();
$tp->addTypesAndCategories('type');
$tp->addTypesAndCategories($cat1);
$tp->addTypesAndCategories($cat2);
$this->assertTrue($tp->getCategories()->contains($cat1));
$this->assertTrue($tp->getCategories()->contains($cat2));
$this->assertCount(2, $tp->getCategories());
$this->assertCount(1, $tp->getTypes());
$this->assertContains('type', $tp->getTypes());
$this->assertCount(3, $tp->getTypesAndCategories());
$this->assertContains($cat1, $tp->getTypesAndCategories());
$this->assertContains($cat2, $tp->getTypesAndCategories());
$this->assertContains('type', $tp->getTypesAndCategories());
// remove type
$tp->removeTypesAndCategories('type');
$tp->removeTypesAndCategories($cat2);
$this->assertTrue($tp->getCategories()->contains($cat1),
"test that cat1 is still present");
$this->assertFalse($tp->getCategories()->contains($cat2));
$this->assertCount(1, $tp->getCategories());
$this->assertCount(0, $tp->getTypes());
$this->assertNotContains('type', $tp->getTypes());
$this->assertCount(1, $tp->getTypesAndCategories());
$this->assertContains($cat1, $tp->getTypesAndCategories());
$this->assertNotContains($cat2, $tp->getTypesAndCategories());
$this->assertNotContains('type', $tp->getTypesAndCategories());
}
public function testSyncingActivityTypes()
{
$tp = new ThirdParty();
$tp->setTypesAndCategories([
'type1',
'type2',
$cat1 = new ThirdPartyCategory(),
$cat2 = new ThirdPartyCategory()
]);
$this->assertTrue($tp->getCategories()->contains($cat1));
$this->assertTrue($tp->getCategories()->contains($cat2));
$this->assertCount(2, $tp->getCategories());
$this->assertCount(2, $tp->getTypes());
$this->assertContains('type1', $tp->getTypes());
$this->assertContains('type2', $tp->getTypes());
$this->assertCount(4, $tp->getTypesAndCategories());
$this->assertContains($cat1, $tp->getTypesAndCategories());
$this->assertContains($cat2, $tp->getTypesAndCategories());
$this->assertContains('type1', $tp->getTypesAndCategories());
$this->assertContains('type2', $tp->getTypesAndCategories());
$tp->setTypesAndCategories([$cat1, 'type1']);
$this->assertTrue($tp->getCategories()->contains($cat1));
$this->assertFalse($tp->getCategories()->contains($cat2));
$this->assertCount(1, $tp->getCategories());
$this->assertCount(1, $tp->getTypes());
$this->assertContains('type1', $tp->getTypes());
$this->assertNotContains('type2', $tp->getTypes());
$this->assertCount(2, $tp->getTypesAndCategories());
$this->assertContains($cat1, $tp->getTypesAndCategories());
$this->assertNotContains($cat2, $tp->getTypesAndCategories());
$this->assertContains('type1', $tp->getTypesAndCategories());
$this->assertNotContains('type2', $tp->getTypesAndCategories());
}
}

View File

@ -1,19 +1,5 @@
services:
Chill\ThirdPartyBundle\Form\ThirdPartyType:
arguments:
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
$typesManager: '@Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager'
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
$om: '@doctrine.orm.entity_manager'
tags:
- { name: form.type }
Chill\ThirdPartyBundle\Form\Type\PickThirdPartyType:
arguments:
$em: '@Doctrine\ORM\EntityManagerInterface'
$urlGenerator: '@Symfony\Component\Routing\Generator\UrlGeneratorInterface'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
$typesManager: '@Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager'
tags:
- { name: form.type }
Chill\ThirdPartyBundle\Form\:
resource: '../../Form/'
autowire: true
autoconfigure: true

View File

@ -67,6 +67,7 @@ No nameCompany given: Aucune raison sociale renseignée
No acronym given: Aucun sigle renseigné
No phone given: Aucun téléphone renseigné
No email given: Aucune adresse courriel renseignée
thirdparty.Any categories: Aucune catégorie
The party is visible in those centers: Le tiers est visible dans ces centres
The party is not visible in any center: Le tiers n'est associé à aucun centre