diff --git a/README.md b/README.md
index 1af00c2..211b31b 100644
--- a/README.md
+++ b/README.md
@@ -71,6 +71,83 @@ If you have defined your own admin controllers, make them extend EasyAdminExtens
Features
--------
+### List filters form
+
+Add filters on list views by configuration.
+
+Consider following Animation entity using such [ValueListTrait](https://github.com/alterphp/components/blob/master/src/AlterPHP/Component/Behavior/ValueListTrait) :
+
+```php
+class Animation
+{
+ use ValueListTrait;
+
+ /**
+ * @var string
+ *
+ * @ORM\Id
+ * @ORM\Column(type="guid")
+ */
+ private $id;
+
+ /**
+ * @var bool
+ *
+ * @ORM\Column(type="boolean", nullable=false)
+ */
+ private $enabled;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(type="string", length=31)
+ */
+ protected $status;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(type="string", length=31, nullable=false)
+ */
+ private $type;
+
+ /**
+ * @var Organization
+ *
+ * @ORM\ManyToOne(targetEntity="App\Entity\Organization", inversedBy="animations")
+ * @ORM\JoinColumn(nullable=false)
+ */
+ private $organization;
+
+ const STATUS_DRAFT = 'draft';
+ const STATUS_PUBLISHED = 'published';
+ const STATUS_OPEN = 'open';
+ const STATUS_ACTIVE = 'active';
+ const STATUS_CLOSED = 'closed';
+ const STATUS_ARCHIVED = 'archived';
+}
+```
+
+Define your filters under `list`.`form_filters` entity configuration. Automatic guesser set up a ChoiceType for filters mapped on boolean (NULL, true, false) and string class properties. ChoiceType for string properties requires either a `choices` label/value array in `type_options` of a `choices_static_callback` static callable that returns label/value choices list.
+
+
+```yaml
+easy_admin:
+ entities:
+ Animation:
+ class: App\Entity\Animation
+ list:
+ form_filters:
+ - enabled
+ - { property: type, type_options: { choices: { Challenge: challenge, Event: event } } }
+ - { property: status, type_options: { choices_static_callback: [getValuesList, [status, true]] } }
+ - organization
+```
+
+Let's see the result !
+
+![Embedded list example](/doc/res/img/list-form-filters.png)
+
### 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/doc/res/img/list-form-filters.png b/doc/res/img/list-form-filters.png
new file mode 100644
index 0000000..8e1eb7b
Binary files /dev/null and b/doc/res/img/list-form-filters.png differ
diff --git a/src/Configuration/ListFormFiltersConfigPass.php b/src/Configuration/ListFormFiltersConfigPass.php
new file mode 100644
index 0000000..5c6a735
--- /dev/null
+++ b/src/Configuration/ListFormFiltersConfigPass.php
@@ -0,0 +1,196 @@
+doctrine = $doctrine;
+ }
+
+ /**
+ * @param array $backendConfig
+ *
+ * @return array
+ */
+ public function process(array $backendConfig): array
+ {
+ if (!isset($backendConfig['entities'])) {
+ return $backendConfig;
+ }
+
+ foreach ($backendConfig['entities'] as $entityName => $entityConfig) {
+ if (!isset($entityConfig['list']['form_filters'])) {
+ continue;
+ }
+
+ $formFilters = array();
+
+ foreach ($entityConfig['list']['form_filters'] as $i => $formFilter) {
+ // Detects invalid config node
+ if (!is_string($formFilter) && !is_array($formFilter)) {
+ throw new \RuntimeException(
+ sprintf(
+ 'The values of the "form_filters" option for the list view of the "%s" entity can only be strings or arrays.',
+ $entityConfig['class']
+ )
+ );
+ }
+
+ // Key mapping
+ if (is_string($formFilter)) {
+ $filterConfig = array('property' => $formFilter);
+ } else {
+ if (!array_key_exists('property', $formFilter)) {
+ throw new \RuntimeException(
+ sprintf(
+ 'One of the values of the "form_filters" option for the "list" view of the "%s" entity does not define the mandatory option "property".',
+ $entityConfig['class']
+ )
+ );
+ }
+
+ $filterConfig = $formFilter;
+ }
+
+ $this->configureFilter($entityConfig['class'], $filterConfig);
+
+ // If type is not configured at this steps => not guessable
+ if (!isset($filterConfig['type'])) {
+ continue;
+ }
+
+ $formFilters[$filterConfig['property']] = $filterConfig;
+ }
+
+ // set form filters config and form !
+ $backendConfig['entities'][$entityName]['list']['form_filters'] = $formFilters;
+ }
+
+ return $backendConfig;
+ }
+
+ private function configureFilter(string $entityClass, array &$filterConfig)
+ {
+ // No need to guess type
+ if (isset($filterConfig['type'])) {
+ return;
+ }
+
+ $em = $this->doctrine->getManagerForClass($entityClass);
+ $entityMetadata = $em->getMetadataFactory()->getMetadataFor($entityClass);
+
+ // Not able to guess type
+ if (
+ !$entityMetadata->hasField($filterConfig['property'])
+ && !$entityMetadata->hasAssociation($filterConfig['property'])
+ ) {
+ return;
+ }
+
+ if ($entityMetadata->hasField($filterConfig['property'])) {
+ $this->configureFieldFilter(
+ $entityClass, $entityMetadata->getFieldMapping($filterConfig['property']), $filterConfig
+ );
+ } elseif ($entityMetadata->hasAssociation($filterConfig['property'])) {
+ $this->configureAssociationFilter(
+ $entityClass, $entityMetadata->getAssociationMapping($filterConfig['property']), $filterConfig
+ );
+ }
+ }
+
+ private function configureFieldFilter(string $entityClass, array $fieldMapping, array &$filterConfig)
+ {
+ switch ($fieldMapping['type']) {
+ case 'boolean':
+ $filterConfig['type'] = ChoiceType::class;
+ $defaultFilterConfigTypeOptions = array(
+ 'choices' => array(
+ 'list_form_filters.default.boolean.true' => true,
+ 'list_form_filters.default.boolean.false' => false,
+ ),
+ 'choice_translation_domain' => 'EasyAdminBundle',
+ );
+ break;
+ case 'string':
+ $filterConfig['type'] = ChoiceType::class;
+ $defaultFilterConfigTypeOptions = array(
+ 'multiple' => true,
+ 'choices' => $this->getChoiceList($entityClass, $filterConfig['property'], $filterConfig),
+ 'attr' => array('data-widget' => 'select2'),
+ );
+ break;
+ default:
+ return;
+ }
+
+ // Merge default type options when defined
+ if (isset($defaultFilterConfigTypeOptions)) {
+ $filterConfig['type_options'] = array_merge(
+ $defaultFilterConfigTypeOptions,
+ isset($filterConfig['type_options']) ? $filterConfig['type_options'] : array()
+ );
+ }
+ }
+
+ private function configureAssociationFilter(string $entityClass, array $associationMapping, array &$filterConfig)
+ {
+ // To-One (EasyAdminAutocompleteType)
+ if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
+ $filterConfig['type'] = EasyAdminAutocompleteType::class;
+ $filterConfig['type_options'] = array_merge(
+ array(
+ 'class' => $associationMapping['targetEntity'],
+ 'multiple' => true,
+ 'attr' => array('data-widget' => 'select2'),
+ ),
+ isset($filterConfig['type_options']) ? $filterConfig['type_options'] : array()
+ );
+ }
+ }
+
+ private function getChoiceList(string $entityClass, string $property, array &$filterConfig)
+ {
+ if (isset($filterConfig['type_options']['choices'])) {
+ $choices = $filterConfig['type_options']['choices'];
+ unset($filterConfig['type_options']['choices']);
+
+ return $choices;
+ }
+
+ if (!isset($filterConfig['type_options']['choices_static_callback'])) {
+ throw new \RuntimeException(
+ sprintf(
+ 'Choice filter field "%s" for entity "%s" must provide either a static callback method returning choice list or choices option.',
+ $property,
+ $entityClass
+ )
+ );
+ }
+
+ $callableParams = array();
+ if (is_string($filterConfig['type_options']['choices_static_callback'])) {
+ $callable = array($entityClass, $filterConfig['type_options']['choices_static_callback']);
+ } else {
+ $callable = array($entityClass, $filterConfig['type_options']['choices_static_callback'][0]);
+ $callableParams = $filterConfig['type_options']['choices_static_callback'][1];
+ }
+ unset($filterConfig['type_options']['choices_static_callback']);
+
+ return forward_static_call_array($callable, $callableParams);
+ }
+}
diff --git a/src/EventListener/PostQueryBuilderSubscriber.php b/src/EventListener/PostQueryBuilderSubscriber.php
index 18f4c3f..ea5da94 100644
--- a/src/EventListener/PostQueryBuilderSubscriber.php
+++ b/src/EventListener/PostQueryBuilderSubscriber.php
@@ -35,6 +35,7 @@ public function onPostListQueryBuilder(GenericEvent $event)
if ($event->hasArgument('request')) {
$this->applyRequestFilters($queryBuilder, $event->getArgument('request')->get('filters', array()));
+ $this->applyFormFilters($queryBuilder, $event->getArgument('request')->get('form_filters', array()));
}
}
@@ -53,7 +54,7 @@ public function onPostSearchQueryBuilder(GenericEvent $event)
}
/**
- * Applies filters on queryBuilder.
+ * Applies request filters on queryBuilder.
*
* @param QueryBuilder $queryBuilder
* @param array $filters
@@ -72,12 +73,48 @@ protected function applyRequestFilters(QueryBuilder $queryBuilder, array $filter
continue;
}
// Sanitize parameter name
- $parameter = 'filter_'.str_replace('.', '_', $field);
+ $parameter = 'request_filter_'.str_replace('.', '_', $field);
$this->filterQueryBuilder($queryBuilder, $field, $parameter, $value);
}
}
+ /**
+ * Applies form filters on queryBuilder.
+ *
+ * @param QueryBuilder $queryBuilder
+ * @param array $filters
+ */
+ protected function applyFormFilters(QueryBuilder $queryBuilder, array $filters = array())
+ {
+ foreach ($filters as $field => $value) {
+ $value = $this->filterEasyadminAutocompleteValue($value);
+ // Empty string and numeric keys is considered as "not applied filter"
+ if (is_int($field) || '' === $value) {
+ 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);
+ }
+ }
+
+ private function filterEasyadminAutocompleteValue($value)
+ {
+ if (!is_array($value) || !isset($value['autocomplete']) || 1 !== count($value)) {
+ return $value;
+ }
+
+ return $value['autocomplete'];
+ }
+
/**
* Filters queryBuilder.
*
diff --git a/src/Helper/ListFormFiltersHelper.php b/src/Helper/ListFormFiltersHelper.php
new file mode 100644
index 0000000..f16ebff
--- /dev/null
+++ b/src/Helper/ListFormFiltersHelper.php
@@ -0,0 +1,62 @@
+formFactory = $formFactory;
+ $this->requestStack = $requestStack;
+ }
+
+ public function getListFiltersForm(array $formFilters): FormInterface
+ {
+ if (!isset($this->listFiltersForm)) {
+ $formBuilder = $this->formFactory->createNamedBuilder('form_filters');
+
+ foreach ($formFilters as $name => $config) {
+ $formBuilder->add(
+ $name,
+ isset($config['type']) ? $config['type'] : null,
+ array_merge(
+ array('required' => false),
+ $config['type_options']
+ )
+ );
+ }
+
+ $this->listFiltersForm = $formBuilder->setMethod('GET')->getForm();
+ $this->listFiltersForm->handleRequest($this->requestStack->getCurrentRequest());
+ }
+
+ return $this->listFiltersForm;
+ }
+}
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index ed3270c..131bdb1 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -34,6 +34,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Resources/public/js/easyadmin-extension.js b/src/Resources/public/js/easyadmin-extension.js
index eb8a050..e355df3 100644
--- a/src/Resources/public/js/easyadmin-extension.js
+++ b/src/Resources/public/js/easyadmin-extension.js
@@ -75,4 +75,13 @@ $(function() {
$('#modal-confirm').modal({ backdrop: true, keyboard: true });
});
+
+ // Deal with panel-heading toggling collapsible panel-body
+ // (@see https://stackoverflow.com/questions/33725181/bootstrap-using-panel-heading-to-collapse-panel-body)
+ $('.panel-heading[data-toggle^="collapse"]').click(function(){
+ var target = $(this).attr('data-target');
+ $(target).collapse('toggle');
+ }).children().click(function(e) {
+ e.stopPropagation();
+ });
});
diff --git a/src/Resources/public/stylesheet/easyadmin-extension.css b/src/Resources/public/stylesheet/easyadmin-extension.css
new file mode 100644
index 0000000..a206186
--- /dev/null
+++ b/src/Resources/public/stylesheet/easyadmin-extension.css
@@ -0,0 +1,3 @@
+#list-form-filters {
+ margin: 10px 0;
+}
diff --git a/src/Resources/translations/EasyAdminBundle.en.xlf b/src/Resources/translations/EasyAdminBundle.en.xlf
index 90f48ed..957c082 100644
--- a/src/Resources/translations/EasyAdminBundle.en.xlf
+++ b/src/Resources/translations/EasyAdminBundle.en.xlf
@@ -7,7 +7,27 @@
Open in a new tab
-
+
+
+
+ Filters
+
+
+
+ Expand / Collapse
+
+
+
+ Filter
+
+
+
+ Yes
+
+
+
+ No
+
diff --git a/src/Resources/translations/EasyAdminBundle.es.xlf b/src/Resources/translations/EasyAdminBundle.es.xlf
index 6e778d8..60b1ce6 100644
--- a/src/Resources/translations/EasyAdminBundle.es.xlf
+++ b/src/Resources/translations/EasyAdminBundle.es.xlf
@@ -7,7 +7,27 @@
Abrir en una nueva pestaña
-
+
+
+
+ Filtros
+
+
+
+ Abrir / Cerrar
+
+
+
+ Filtro
+
+
+
+ Si
+
+
+
+ No
+
diff --git a/src/Resources/translations/EasyAdminBundle.fr.xlf b/src/Resources/translations/EasyAdminBundle.fr.xlf
index d4691ce..71438e7 100644
--- a/src/Resources/translations/EasyAdminBundle.fr.xlf
+++ b/src/Resources/translations/EasyAdminBundle.fr.xlf
@@ -2,13 +2,33 @@
-
+
Ouvrir dans un nouvel onglet
-
-
+
+
+
+ Filtres
+
+
+
+ Ouvrir / Fermer
+
+
+
+ Filtrer
+
+
+
+ Oui
+
+
+
+ Non
+
+
Êtes-vous sûr(e) ?
diff --git a/src/Resources/translations/EasyAdminBundle.hu.xlf b/src/Resources/translations/EasyAdminBundle.hu.xlf
index 445478e..93fa99e 100644
--- a/src/Resources/translations/EasyAdminBundle.hu.xlf
+++ b/src/Resources/translations/EasyAdminBundle.hu.xlf
@@ -7,7 +7,27 @@
Megnyitás új lapon
-
+
+
+
+ Szűrők
+
+
+
+ Nyitás / Bezárás
+
+
+
+ Szűrő
+
+
+
+ Igen
+
+
+
+ Nem
+
diff --git a/src/Resources/translations/EasyAdminBundle.it.xlf b/src/Resources/translations/EasyAdminBundle.it.xlf
index 800c2a0..4ba80ec 100644
--- a/src/Resources/translations/EasyAdminBundle.it.xlf
+++ b/src/Resources/translations/EasyAdminBundle.it.xlf
@@ -7,7 +7,27 @@
Apri in una nuova scheda
-
+
+
+
+ Filtri
+
+
+
+ Apri / Chiudi
+
+
+
+ Filtro
+
+
+
+ Si
+
+
+
+ Non
+
diff --git a/src/Resources/views/default/layout.html.twig b/src/Resources/views/default/layout.html.twig
index fee9a10..e9307e5 100644
--- a/src/Resources/views/default/layout.html.twig
+++ b/src/Resources/views/default/layout.html.twig
@@ -1,5 +1,10 @@
{% extends '@BaseEasyAdmin/default/layout.html.twig' %}
+{% block head_stylesheets %}
+ {{ parent() }}
+
+{% endblock %}
+
{% block head_javascript %}
{{ parent() }}
diff --git a/src/Resources/views/default/list.html.twig b/src/Resources/views/default/list.html.twig
index 54044f7..e81e84b 100644
--- a/src/Resources/views/default/list.html.twig
+++ b/src/Resources/views/default/list.html.twig
@@ -5,6 +5,18 @@
filters: requestFilters
}) %}
+{% block request_parameters_as_hidden %}
+ {% for field, value in requestFilters %}
+ {% if value is iterable %}
+ {% for val in value %}
+
+ {% endfor %}
+ {% else %}
+
+ {% endif %}
+ {% endfor %}
+{% endblock %}
+
{% block content_title_wrapper %}
{{ block('content_title') }}
@@ -46,16 +58,62 @@
{{ parent() }}
{% endblock %}
-{# Adds request filters to the serach form #}
+{# Adds request filters to the search form #}
{% block search_form %}
- {% for field, value in requestFilters %}
- {% if value is iterable %}
- {% for val in value %}
-
- {% endfor %}
- {% else %}
-
- {% endif %}
- {% endfor %}
+ {{ block('request_parameters_as_hidden') }}
+ {{ parent() }}
+{% endblock %}
+
+{% block list_form_filters %}
+ {% if _entity_config.list.form_filters is defined and _entity_config.list.form_filters is not empty %}
+ {% set list_form_filters = list_form_filters(_entity_config.list.form_filters) %}
+