diff --git a/README.md b/README.md index 538dc6d2..0d554630 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,13 @@ class Animation */ private $type; + /** + * @var int + * + * @ORM\Column(type="integer", nullable=false) + */ + private $maxSubscriptions; + /** * @var Organization * @@ -169,13 +176,44 @@ Let's see the result ! ![Embedded list example](/doc/res/img/list-form-filters.png) +#### Automatic list filter guesser + Guesser for list form filters are based on mapped entity property : * _boolean_: guessed filter is a choice list (null, Yes, No) * _string_: guessed filter is multiple choice list that requires either `choices` (value/label array) or `choices_static_callback` (static callback from entity class returning a value/label array) in `type_options`. +* _integer_, _smallint_, _bigint_: guessed filter is an integer input +* _decimal_, _float_: guessed filter is a number input * _*-to-one-relation_: guessed filter is a multiple autocomplete of relation target entity. Filters form's method is GET and submitted through `form_filter` parameter. It is transmitted to the referer used for post update/delete/create redirection AND for search ! +#### List filter operator + +By default, list filter use `equals` operator or `in` for multiple value filters. + +But you can use more operators with the `operator` attribute : + +```yaml +entities: + Animation: + class: App\Entity\Animation + list: + form_filters: + - { name: maxSubscriptionGTE, property: maxSubscriptions, label: 'Max subscriptions >=', operator: gte } + - { name: maxSubscriptionLTE, property: maxSubscriptions, label: 'Max subscriptions <=', operator: lte } +``` + +Available built-in operators are listed in `AlterPHP\EasyAdminExtensionBundle\Model\ListFilter` class, as constant `OPERATOR_*` : +* __equals__: Is equal to +* __not__: Is different of +* __in__: Is in (`array` or Doctrine `Collection` expected) +* __notin__: Is not in (`array` or Doctrine `Collection` expected) +* __gt__: Is greater than +* __gte__: Is greater than or equal to +* __lt__: Is lower than +* __lte__: Is lower than or equal to + + ### Filter list and search on request parameters * EasyAdmin allows filtering list with `dql_filter` configuration entry. But this is not dynamic and must be configured as an apart list in `easy_admin` configuration.* diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 00000000..b579259d --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,31 @@ +# UPGRADE guide for EasyAdminExtension bundle + +## v2.1.0 + +List filters form have been improved, with minor BC breaks : + +* Label must be configured on the root level, not in the `type_options` attribiute. + +__BEFORE__ + +```yaml +easy_admin: + entities: + MyEntity: + class: App\Entity\MyEntity + list: + form_filters: + - { name: myFilter, property: status, type_options: { label: 'Filter on status' } } +``` + +__AFTER__ + +```yaml +easy_admin: + entities: + MyEntity: + class: App\Entity\MyEntity + list: + form_filters: + - { name: myFilter, property: status, label: 'Filter on status' } +``` diff --git a/src/Configuration/ListFormFiltersConfigPass.php b/src/Configuration/ListFormFiltersConfigPass.php index 350096e7..ccbc6c3f 100644 --- a/src/Configuration/ListFormFiltersConfigPass.php +++ b/src/Configuration/ListFormFiltersConfigPass.php @@ -2,11 +2,15 @@ namespace AlterPHP\EasyAdminExtensionBundle\Configuration; +use AlterPHP\EasyAdminExtensionBundle\Model\ListFilter; use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\DBAL\Types\type as DBALType; use Doctrine\ORM\Mapping\ClassMetadataInfo; use EasyCorp\Bundle\EasyAdminBundle\Configuration\ConfigPassInterface; use EasyCorp\Bundle\EasyAdminBundle\Form\Type\EasyAdminAutocompleteType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; /** * Guess form types for list form filters. @@ -52,7 +56,7 @@ public function process(array $backendConfig): array // Key mapping if (\is_string($formFilter)) { - $filterConfig = ['property' => $formFilter]; + $filterConfig = ['name' => $formFilter, 'property' => $formFilter]; } else { if (!\array_key_exists('property', $formFilter)) { throw new \RuntimeException( @@ -64,6 +68,8 @@ public function process(array $backendConfig): array } $filterConfig = $formFilter; + // Auto set name with property value + $filterConfig['name'] = $filterConfig['name'] ?? $filterConfig['property']; } $this->configureFilter( @@ -77,7 +83,7 @@ public function process(array $backendConfig): array continue; } - $formFilters[$filterConfig['property']] = $filterConfig; + $formFilters[$filterConfig['name']] = $filterConfig; } // set form filters config and form ! @@ -89,11 +95,6 @@ public function process(array $backendConfig): array private function configureFilter(string $entityClass, array &$filterConfig, string $translationDomain) { - // No need to guess type - if (isset($filterConfig['type'])) { - return; - } - $em = $this->doctrine->getManagerForClass($entityClass); $entityMetadata = $em->getMetadataFactory()->getMetadataFor($entityClass); @@ -116,11 +117,13 @@ private function configureFilter(string $entityClass, array &$filterConfig, stri } } - private function configureFieldFilter(string $entityClass, array $fieldMapping, array &$filterConfig, string $translationDomain) - { + private function configureFieldFilter( + string $entityClass, array $fieldMapping, array &$filterConfig, string $translationDomain + ) { switch ($fieldMapping['type']) { - case 'boolean': - $filterConfig['type'] = ChoiceType::class; + case DBALType::BOOLEAN: + $filterConfig['operator'] = $filterConfig['operator'] ?? ListFilter::OPERATOR_EQUALS; + $filterConfig['type'] = $filterConfig['type'] ?? ChoiceType::class; $defaultFilterConfigTypeOptions = [ 'choices' => [ 'list_form_filters.default.boolean.true' => true, @@ -129,19 +132,43 @@ private function configureFieldFilter(string $entityClass, array $fieldMapping, 'choice_translation_domain' => 'EasyAdminBundle', ]; break; - case 'string': - $filterConfig['type'] = ChoiceType::class; + case DBALType::STRING: + $filterConfig['operator'] = $filterConfig['operator'] ?? ListFilter::OPERATOR_IN; + $filterConfig['type'] = $filterConfig['type'] ?? ChoiceType::class; $defaultFilterConfigTypeOptions = [ - 'multiple' => true, + 'multiple' => in_array($filterConfig['operator'], [ListFilter::OPERATOR_IN, ListFilter::OPERATOR_NOTIN]), + 'placeholder' => '-', 'choices' => $this->getChoiceList($entityClass, $filterConfig['property'], $filterConfig), 'attr' => ['data-widget' => 'select2'], 'choice_translation_domain' => $translationDomain, ]; break; + case DBALType::SMALLINT: + case DBALType::INTEGER: + case DBALType::BIGINT: + $filterConfig['operator'] = $filterConfig['operator'] ?? ListFilter::OPERATOR_EQUALS; + $filterConfig['type'] = $filterConfig['type'] ?? IntegerType::class; + $defaultFilterConfigTypeOptions = []; + break; + case DBALType::DECIMAL: + case DBALType::FLOAT: + $filterConfig['operator'] = $filterConfig['operator'] ?? ListFilter::OPERATOR_EQUALS; + $filterConfig['type'] = $filterConfig['type'] ?? NumberType::class; + $defaultFilterConfigTypeOptions = []; + break; default: return; } + // Auto set multiple on ChoiceType when operator requires array + if (ChoiceType::class === $filterConfig['type']) { + $defaultFilterConfigTypeOptions['choices'] = $defaultFilterConfigTypeOptions['choices'] ?? $this->getChoiceList($entityClass, $filterConfig['property'], $filterConfig); + + if (in_array($filterConfig['operator'], [ListFilter::OPERATOR_IN, ListFilter::OPERATOR_NOTIN])) { + $defaultFilterConfigTypeOptions['multiple'] = $defaultFilterConfigTypeOptions['multiple'] ?? true; + } + } + // Merge default type options when defined if (null !== $defaultFilterConfigTypeOptions) { $filterConfig['type_options'] = \array_merge( @@ -155,7 +182,8 @@ private function configureAssociationFilter(string $entityClass, array $associat { // To-One (EasyAdminAutocompleteType) if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { - $filterConfig['type'] = EasyAdminAutocompleteType::class; + $filterConfig['operator'] = $filterConfig['operator'] ?? ListFilter::OPERATOR_IN; + $filterConfig['type'] = $filterConfig['type'] ?? EasyAdminAutocompleteType::class; $filterConfig['type_options'] = \array_merge( [ 'class' => $associationMapping['targetEntity'], diff --git a/src/Configuration/ShortFormTypeConfigPass.php b/src/Configuration/ShortFormTypeConfigPass.php index 14c76c6d..2af9e79f 100644 --- a/src/Configuration/ShortFormTypeConfigPass.php +++ b/src/Configuration/ShortFormTypeConfigPass.php @@ -3,6 +3,7 @@ namespace AlterPHP\EasyAdminExtensionBundle\Configuration; use AlterPHP\EasyAdminExtensionBundle\Form\Type\EasyAdminEmbeddedListType; +use AlterPHP\EasyAdminExtensionBundle\Form\Type\ListFilterType; use AlterPHP\EasyAdminExtensionBundle\Form\Type\Security\AdminRolesType; use EasyCorp\Bundle\EasyAdminBundle\Configuration\ConfigPassInterface; use EasyCorp\Bundle\EasyAdminBundle\Form\Util\FormTypeHelper; @@ -23,6 +24,7 @@ class ShortFormTypeConfigPass implements ConfigPassInterface private static $nativeShortFormTypes = [ 'embedded_list' => EasyAdminEmbeddedListType::class, 'admin_roles' => AdminRolesType::class, + 'list_filter' => ListFilterType::class, ]; public function __construct(array $customFormTypes = []) diff --git a/src/EventListener/PostQueryBuilderSubscriber.php b/src/EventListener/PostQueryBuilderSubscriber.php index 984d3ab7..dc074720 100644 --- a/src/EventListener/PostQueryBuilderSubscriber.php +++ b/src/EventListener/PostQueryBuilderSubscriber.php @@ -2,7 +2,7 @@ namespace AlterPHP\EasyAdminExtensionBundle\EventListener; -use Doctrine\Common\Collections\Collection; +use AlterPHP\EasyAdminExtensionBundle\Model\ListFilter; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\QueryBuilder; use EasyCorp\Bundle\EasyAdminBundle\Event\EasyAdminEvents; @@ -93,16 +93,11 @@ protected function applyRequestFilters(QueryBuilder $queryBuilder, array $filter if ('' === $value || \is_int($field)) { continue; } - // Add root entity alias if none provided - $field = false === \strpos($field, '.') ? $queryBuilder->getRootAlias().'.'.$field : $field; - // Checks if filter is directly appliable on queryBuilder - if (!$this->isFilterAppliable($queryBuilder, $field)) { - continue; - } - // Sanitize parameter name - $parameter = 'request_filter_'.\str_replace('.', '_', $field); - $this->filterQueryBuilder($queryBuilder, $field, $parameter, $value); + $operator = is_array($value) ? ListFilter::OPERATOR_IN : ListFilter::OPERATOR_EQUALS; + $listFilter = ListFilter::createFromRequest($field, $operator, $value); + + $this->filterQueryBuilder($queryBuilder, $field, $listFilter); } } @@ -114,74 +109,118 @@ protected function applyRequestFilters(QueryBuilder $queryBuilder, array $filter */ protected function applyFormFilters(QueryBuilder $queryBuilder, array $filters = []) { - foreach ($filters as $field => $value) { - $value = $this->filterEasyadminAutocompleteValue($value); - // Empty string and numeric keys is considered as "not applied filter" - if (null === $value || '' === $value || \is_int($field)) { + foreach ($filters as $field => $listFilter) { + if (null === $listFilter) { continue; } - // Add root entity alias if none provided - $field = false === \strpos($field, '.') ? $queryBuilder->getRootAlias().'.'.$field : $field; - // Checks if filter is directly appliable on queryBuilder - if (!$this->isFilterAppliable($queryBuilder, $field)) { - continue; - } - // Sanitize parameter name - $parameter = 'form_filter_'.\str_replace('.', '_', $field); - $this->filterQueryBuilder($queryBuilder, $field, $parameter, $value); + $this->filterQueryBuilder($queryBuilder, $field, $listFilter); } } - private function filterEasyadminAutocompleteValue($value) - { - if (!\is_array($value) || !isset($value['autocomplete']) || 1 !== \count($value)) { - return $value; - } - - return $value['autocomplete']; - } - /** * Filters queryBuilder. * * @param QueryBuilder $queryBuilder * @param string $field - * @param string $parameter - * @param mixed $value + * @param ListFilter $listFilter */ - protected function filterQueryBuilder(QueryBuilder $queryBuilder, string $field, string $parameter, $value) + protected function filterQueryBuilder(QueryBuilder $queryBuilder, string $field, ListFilter $listFilter) { - switch (true) { - // Multiple values leads to IN statement - case $value instanceof Collection: - case \is_array($value): + $value = $this->filterEasyadminAutocompleteValue($listFilter->getValue()); + // Empty string and numeric keys is considered as "not applied filter" + if (null === $value || '' === $value || \is_int($field)) { + return; + } + + // Add root entity alias if none provided + $queryField = $listFilter->getProperty(); + if (false === \strpos($queryField, '.')) { + $queryField = $queryBuilder->getRootAlias().'.'.$queryField; + } + + // Checks if filter is directly appliable on queryBuilder + if (!$this->isFilterAppliable($queryBuilder, $queryField)) { + return; + } + + $operator = $listFilter->getOperator(); + // Sanitize parameter name + $parameter = 'form_filter_'.\str_replace('.', '_', $field); + + switch ($operator) { + case ListFilter::OPERATOR_EQUALS: + if ('_NULL' === $value) { + $queryBuilder->andWhere(\sprintf('%s IS NULL', $queryField)); + } elseif ('_NOT_NULL' === $value) { + $queryBuilder->andWhere(\sprintf('%s IS NOT NULL', $queryField)); + } else { + $queryBuilder + ->andWhere(\sprintf('%s %s :%s', $queryField, '=', $parameter)) + ->setParameter($parameter, $value) + ; + } + break; + case ListFilter::OPERATOR_NOT: + $queryBuilder + ->andWhere(\sprintf('%s %s :%s', $queryField, '!=', $parameter)) + ->setParameter($parameter, $value) + ; + break; + case ListFilter::OPERATOR_IN: + // Checks that $value is not an empty Traversable if (0 < \count($value)) { - $filterDqlPart = $field.' IN (:'.$parameter.')'; + $queryBuilder + ->andWhere(\sprintf('%s %s (:%s)', $queryField, 'IN', $parameter)) + ->setParameter($parameter, $value) + ; } break; - // Special value for NULL evaluation - case '_NULL' === $value: - $parameter = null; - $filterDqlPart = $field.' IS NULL'; + case ListFilter::OPERATOR_NOTIN: + // Checks that $value is not an empty Traversable + if (0 < \count($value)) { + $queryBuilder + ->andWhere(\sprintf('%s %s (:%s)', $queryField, 'NOT IN', $parameter)) + ->setParameter($parameter, $value) + ; + } break; - // Special value for NOT NULL evaluation - case '_NOT_NULL' === $value: - $parameter = null; - $filterDqlPart = $field.' IS NOT NULL'; + case ListFilter::OPERATOR_GT: + $queryBuilder + ->andWhere(\sprintf('%s %s :%s', $queryField, '>', $parameter)) + ->setParameter($parameter, $value) + ; break; - // Default is equality - default: - $filterDqlPart = $field.' = :'.$parameter; + case ListFilter::OPERATOR_GTE: + $queryBuilder + ->andWhere(\sprintf('%s %s :%s', $queryField, '>=', $parameter)) + ->setParameter($parameter, $value) + ; + break; + case ListFilter::OPERATOR_LT: + $queryBuilder + ->andWhere(\sprintf('%s %s :%s', $queryField, '<', $parameter)) + ->setParameter($parameter, $value) + ; break; + case ListFilter::OPERATOR_LTE: + $queryBuilder + ->andWhere(\sprintf('%s %s :%s', $queryField, '<=', $parameter)) + ->setParameter($parameter, $value) + ; + break; + default: + throw new \RuntimeException(\sprintf('Operator "%s" is not supported !', $operator)); } + } - if (isset($filterDqlPart)) { - $queryBuilder->andWhere($filterDqlPart); - if (null !== $parameter) { - $queryBuilder->setParameter($parameter, $value); - } + protected function filterEasyadminAutocompleteValue($value) + { + if (!\is_array($value) || !isset($value['autocomplete']) || 1 !== \count($value)) { + return $value; } + + return $value['autocomplete']; } /** diff --git a/src/Form/Type/ListFilterType.php b/src/Form/Type/ListFilterType.php new file mode 100644 index 00000000..d82b0c01 --- /dev/null +++ b/src/Form/Type/ListFilterType.php @@ -0,0 +1,50 @@ +add('value', $options['input_type'], $options['input_type_options'] + [ + 'label' => false, + 'required' => false, + ]) + ; + + $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($options) { + $listFilter = $event->getData(); + $form = $event->getForm(); + + if (null !== $listFilter) { + $listFilter->setOperator($options['operator']); + if (isset($options['property']) && !empty($options['property'])) { + $listFilter->setProperty($options['property']); + } else { + $listFilter->setProperty($form->getName()); + } + } + }); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'operator' => ListFilter::OPERATOR_EQUALS, + 'data_class' => ListFilter::class, + 'input_type' => TextType::class, + 'input_type_options' => [], + ]); + $resolver->setDefined(['property']); + $resolver->setAllowedValues('operator', ListFilter::getOperatorsList()); + } +} diff --git a/src/Helper/ListFormFiltersHelper.php b/src/Helper/ListFormFiltersHelper.php index c4f8f123..aea5033e 100644 --- a/src/Helper/ListFormFiltersHelper.php +++ b/src/Helper/ListFormFiltersHelper.php @@ -4,6 +4,7 @@ namespace AlterPHP\EasyAdminExtensionBundle\Helper; +use AlterPHP\EasyAdminExtensionBundle\Form\Type\ListFilterType; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormFactory; use Symfony\Component\Form\FormInterface; @@ -58,14 +59,20 @@ public function getListFormFilters(array $formFilters): FormInterface ); foreach ($formFilters as $name => $config) { - $formBuilder->add( - $name, - $config['type'] ?? null, - \array_merge( - ['required' => false], - $config['type_options'] ?? [] - ) - ); + $listFilterformOptions = [ + 'label' => $config['label'] ?? null, + 'required' => false, + 'input_type' => $config['type'], + 'input_type_options' => $config['type_options'] ?? [], + ]; + if (isset($config['operator'])) { + $listFilterformOptions['operator'] = $config['operator']; + } + if (isset($config['property'])) { + $listFilterformOptions['property'] = $config['property']; + } + + $formBuilder->add($name, ListFilterType::class, $listFilterformOptions); } $this->listFiltersForm = $formBuilder->setMethod('GET')->getForm(); diff --git a/src/Model/ListFilter.php b/src/Model/ListFilter.php new file mode 100644 index 00000000..7202d5ae --- /dev/null +++ b/src/Model/ListFilter.php @@ -0,0 +1,113 @@ +setProperty($property) + ->setOperator($operator) + ->setValue($value) + ; + + return $listFilter; + } + + /** + * Returns operators list. + * + * @return array + */ + public static function getOperatorsList() + { + // Build $operatorValues if this is the first call + if (null === static::$operatorValues) { + static::$operatorValues = []; + $refClass = new \ReflectionClass(static::class); + $classConstants = $refClass->getConstants(); + $className = $refClass->getShortName(); + + $constantPrefix = 'OPERATOR_'; + foreach ($classConstants as $key => $val) { + if (\substr($key, 0, \strlen($constantPrefix)) === $constantPrefix) { + static::$operatorValues[] = $val; + } + } + } + + return static::$operatorValues; + } + + public function getOperator() + { + return $this->operator; + } + + public function setOperator(string $operator) + { + if (!in_array($operator, static::getOperatorsList())) { + throw new \InvalidArgumentException(sprintf('Operator "%s" is not allowed !', $operator)); + } + + $this->operator = $operator; + + return $this; + } + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + $this->value = $value; + + return $this; + } + + public function hasProperty() + { + return !empty($this->property); + } + + public function getProperty() + { + return $this->property; + } + + public function setProperty($property) + { + $this->property = $property; + + return $this; + } +} diff --git a/src/Resources/public/stylesheet/easyadmin-extension.css b/src/Resources/public/stylesheet/easyadmin-extension.css index 0c103eda..2d8536eb 100644 --- a/src/Resources/public/stylesheet/easyadmin-extension.css +++ b/src/Resources/public/stylesheet/easyadmin-extension.css @@ -4,10 +4,12 @@ #list-form-filters-header[data-toggle^="collapse"] { cursor: pointer; } - #list-form-filters-form { padding: 5px; } +#list-form-filters-form .field-list_filter > .form-widget > .form-widget-compound > .collection-empty { + display: none; +} /* SHOW vertical */ .form-vertical .form-group { diff --git a/tests/Controller/EmbeddedListTest.php b/tests/Controller/EmbeddedListTest.php index 97219721..d3c65cc0 100644 --- a/tests/Controller/EmbeddedListTest.php +++ b/tests/Controller/EmbeddedListTest.php @@ -48,7 +48,10 @@ public function testRequestSingleFilterIsApplied() $crawler = $this->requestListView('Product', ['entity.enabled' => false]); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestNoFieldFilterCausesNoError() @@ -56,7 +59,10 @@ public function testRequestNoFieldFilterCausesNoError() $crawler = $this->requestListView('Product', ['entity.foo' => 'bar']); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(15, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '100 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestManyFiltersAreApplied() @@ -66,7 +72,10 @@ public function testRequestManyFiltersAreApplied() ); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(5, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '5 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestFilterWithoutAliasIsCompletedAndApplied() @@ -74,7 +83,10 @@ public function testRequestFilterWithoutAliasIsCompletedAndApplied() $crawler = $this->requestListView('Product', ['enabled' => false]); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestFiltersArePassedToSearchForm() @@ -125,7 +137,10 @@ public function testRequestFilterIsAppliedToSearchAction() { $crawler = $this->requestSearchView('ref000', 'Product', ['entity.enabled' => false]); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestNullFilterIsApplied() @@ -135,7 +150,10 @@ public function testRequestNullFilterIsApplied() ); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestNotNullFilterIsApplied() diff --git a/tests/Controller/ListFormFiltersTest.php b/tests/Controller/ListFormFiltersTest.php index 33bdada2..537a1649 100644 --- a/tests/Controller/ListFormFiltersTest.php +++ b/tests/Controller/ListFormFiltersTest.php @@ -21,25 +21,131 @@ public function testListFiltersAreDisplaid() $listFormFiltersCrawler = $crawler->filter('#list-form-filters'); - $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_oddEven[multiple]')->count()); - $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_category_autocomplete[multiple]')->count()); - $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_replenishmentType[multiple]')->count()); - $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_enabled')->count()); + $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_oddEven_value[multiple]')->count()); + $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_category_value_autocomplete[multiple]')->count()); + $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_replenishmentType_value[multiple]')->count()); + $this->assertSame(1, $listFormFiltersCrawler->filter('select#form_filters_enabled_value')->count()); } public function testFormSingleFilterIsApplied() { - $crawler = $this->requestListView('Product', [], ['enabled' => false]); + $crawler = $this->requestListView('Product', [], ['enabled' => ['value' => false]]); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testFormSingleEasyadminAutocompleteFilterIsApplied() { - $crawler = $this->requestListView('Product', [], ['category' => ['autocomplete' => [1]]]); + $crawler = $this->requestListView('Product', [], ['category' => ['value' => ['autocomplete' => [1]]]]); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); + } + + public function testListFilterGreaterThanOperator() + { + $crawler = $this->requestListView('Product', [], ['priceGreaterThan' => ['value' => 5100]]); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertContains( + '49 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); + } + + public function testListFilterGreaterThanOrEqualsOperator() + { + $crawler = $this->requestListView('Product', [], ['priceGreaterThanOrEquals' => ['value' => 5100]]); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertContains( + '50 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); + } + + public function testListFilterLowerThanOperator() + { + $crawler = $this->requestListView('Product', [], ['priceLowerThan' => ['value' => 5100]]); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertContains( + '50 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); + } + + public function testListFilterLowerThanOrEqualsOperator() + { + $crawler = $this->requestListView('Product', [], ['priceLowerThanOrEquals' => ['value' => 5100]]); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertContains( + '51 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); + } + + public function testListFilterNotOperator() + { + $crawler = $this->requestListView('Product', [], ['notOddEven' => ['value' => 'even']]); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertContains( + '75 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); + } + + public function testListFilterNotInOperator() + { + $crawler = $this->requestListView( + 'Product', + [], + ['notInPhone' => ['value' => ['0123456789-0', '0123456789-1', '0123456789-2', '0123456789-3']]] + ); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertContains( + '54 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); + } + + public function testListFilterCombinedOperator() + { + $crawler = $this->requestListView( + 'Product', + [], + ['priceLowerThanOrEquals' => ['value' => 5100], 'priceGreaterThan' => ['value' => 3000]] + ); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertContains( + '21 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); + } + + public function testListFilterDefaultInteger() + { + $crawler = $this->requestListView( + 'Product', + [], + ['stock' => ['value' => 30]] + ); + + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + $this->assertContains( + '25 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } } diff --git a/tests/Controller/RequestParametersTest.php b/tests/Controller/RequestParametersTest.php index e50f696a..84ee6cfc 100644 --- a/tests/Controller/RequestParametersTest.php +++ b/tests/Controller/RequestParametersTest.php @@ -18,7 +18,10 @@ public function testRequestSingleFilterIsApplied() $crawler = $this->requestListView('Product', ['entity.enabled' => false]); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestNoFieldFilterCausesNoError() @@ -26,7 +29,10 @@ public function testRequestNoFieldFilterCausesNoError() $crawler = $this->requestListView('Product', ['entity.foo' => 'bar']); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(15, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '100 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestManyFiltersAreApplied() @@ -36,7 +42,10 @@ public function testRequestManyFiltersAreApplied() ); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(5, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '5 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestFilterWithoutAliasIsCompletedAndApplied() @@ -44,7 +53,10 @@ public function testRequestFilterWithoutAliasIsCompletedAndApplied() $crawler = $this->requestListView('Product', ['enabled' => false]); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestFiltersArePassedToSearchForm() @@ -96,7 +108,10 @@ public function testRequestFilterIsAppliedToSearchAction() $crawler = $this->requestSearchView('ref000', 'Product', ['entity.enabled' => false]); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestNullFilterIsApplied() @@ -106,7 +121,10 @@ public function testRequestNullFilterIsApplied() ); $this->assertSame(200, $this->client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('#main tr[data-id]')->count()); + $this->assertContains( + '10 results', + $crawler->filter('section.content-footer .list-pagination-counter')->text() + ); } public function testRequestNotNullFilterIsApplied() diff --git a/tests/Fixtures/App/config/config_list_form_filters.yaml b/tests/Fixtures/App/config/config_list_form_filters.yaml index 1ccec7b6..a00dbab6 100644 --- a/tests/Fixtures/App/config/config_list_form_filters.yaml +++ b/tests/Fixtures/App/config/config_list_form_filters.yaml @@ -8,8 +8,38 @@ easy_admin: Product: class: AppTestBundle\Entity\FunctionalTests\Product list: + max_results: 200 + fields: + - id + - ean + - oddEven + - price + - replenishmentType + - phone form_filters: - { property: oddEven, type_options: { choices: {Odd: odd, Even: even} } } + - { name: notOddEven, property: oddEven, operator: not, type_options: { choices: {Odd: odd, Even: even} } } - { property: replenishmentType, type_options: { choices_static_callback: [getReplenishmentTypeValues, [true]] } } - enabled - category + - stock + - { name: priceGreaterThan, property: price, operator: gt } + - { name: priceGreaterThanOrEquals, property: price, operator: gte } + - { name: priceLowerThan, property: price, operator: lt } + - { name: priceLowerThanOrEquals, property: price, operator: lte } + - name: notInPhone + property: phone + operator: notin + type: choice + type_options: + choices: + '0123456789-0': '0123456789-0' + '0123456789-1': '0123456789-1' + '0123456789-2': '0123456789-2' + '0123456789-3': '0123456789-3' + '0123456789-4': '0123456789-4' + '0123456789-5': '0123456789-5' + '0123456789-6': '0123456789-6' + '0123456789-7': '0123456789-7' + '0123456789-8': '0123456789-8' + '0123456789-9': '0123456789-9' diff --git a/tests/Fixtures/AppTestBundle/DataFixtures/AppFixtures.php b/tests/Fixtures/AppTestBundle/DataFixtures/AppFixtures.php index 73e6d1b8..025c3873 100644 --- a/tests/Fixtures/AppTestBundle/DataFixtures/AppFixtures.php +++ b/tests/Fixtures/AppTestBundle/DataFixtures/AppFixtures.php @@ -168,13 +168,14 @@ private function createProducts(): array $product->setReference('ref'.\str_pad($i, 6, '0', STR_PAD_LEFT)); $product->setName($this->getRandomName()); $product->setReplenishmentType($this->getReplenishmentType()); - $product->setPrice($this->getRandomPrice()); + $product->setPrice($i * 100); + $product->setStock(($i % 4) * 10); $product->setTags($this->getRandomTags()); $product->setEan($this->getRandomEan()); $product->setCategory($category); $product->setDescription($this->getRandomDescription()); $product->setHtmlFeatures($this->getRandomHtmlFeatures()); - $product->setPhone($i <= 10 ? null : '0123456789'); + $product->setPhone($i <= 10 ? null : '0123456789-'.($i % 10)); $this->addReference('product-'.$i, $product); $products[] = $product; diff --git a/tests/Fixtures/AppTestBundle/Entity/FunctionalTests/Product.php b/tests/Fixtures/AppTestBundle/Entity/FunctionalTests/Product.php index d9fd6392..d5a8c7d5 100644 --- a/tests/Fixtures/AppTestBundle/Entity/FunctionalTests/Product.php +++ b/tests/Fixtures/AppTestBundle/Entity/FunctionalTests/Product.php @@ -103,6 +103,14 @@ class Product */ protected $price = 0.0; + /** + * The stock of the product. + * + * @var int + * @ORM\Column(type="integer") + */ + protected $stock = 0; + /** * The reference of the product. * @@ -426,6 +434,26 @@ public function getPrice() return $this->price; } + /** + * Set the stock. + * + * @param int $stock + */ + public function setStock($stock) + { + $this->stock = $stock; + } + + /** + * Get the stock of the product. + * + * @return int + */ + public function getStock() + { + return $this->stock; + } + /** * Set the list of the tags. *