diff --git a/src/Grid/Filter.php b/src/Grid/Filter.php index ea9a2f1..d1249e9 100644 --- a/src/Grid/Filter.php +++ b/src/Grid/Filter.php @@ -7,14 +7,17 @@ class Filter public string $formFieldName; public $callback; public bool $enabled; + public ?string $group; public function __construct( string $formFieldName, callable $callback, - bool $enabled = true + bool $enabled = true, + ?string $group = null ) { $this->formFieldName = $formFieldName; $this->callback = $callback; $this->enabled = $enabled; + $this->group = $group; } } diff --git a/src/Grid/Grid.php b/src/Grid/Grid.php index c2d4f5f..b47fca7 100644 --- a/src/Grid/Grid.php +++ b/src/Grid/Grid.php @@ -17,6 +17,7 @@ class Grid private string $batchMethod; private string $theme; private $rowAttributesCallback = null; + private array $filterLayout; private Request $request; private PaginationInterface $pagination; @@ -30,6 +31,7 @@ public function __construct( string $batchMethod = 'POST', string $batchActionsTokenId, ?callable $rowAttributesCallback = null, + array $filterLayout = [], ) { $this->columns = $columns; $this->request = $request; @@ -39,6 +41,7 @@ public function __construct( $this->batchActionsTokenId = $batchActionsTokenId; $this->theme = $theme; $this->rowAttributesCallback = $rowAttributesCallback; + $this->filterLayout = $filterLayout; } public function getColumns(): array @@ -81,6 +84,11 @@ public function getTheme(): string return $this->theme; } + public function getFilterLayout(): array + { + return $this->filterLayout; + } + public function getRowAttributes($item, bool $keepAsArray = false): null|array|string { if (!is_callable($this->rowAttributesCallback)) { diff --git a/src/Grid/GridBuilder.php b/src/Grid/GridBuilder.php index fe4b8a9..87af95e 100644 --- a/src/Grid/GridBuilder.php +++ b/src/Grid/GridBuilder.php @@ -145,13 +145,60 @@ public function removeColumn(string $name): self return $this; } - public function addFilter(string $formFieldName, callable $callback, bool $enabled = true): self + public function addFilter(string $formFieldName, callable $callback, bool $enabled = true, ?string $group = null): self { - $this->filters[] = new Filter($formFieldName, $callback, $enabled); + $this->filters[] = new Filter($formFieldName, $callback, $enabled, $group); return $this; } + /** + * Converts the flat list of filters into a layout array consumed by Twig. + * + * Each entry in the returned array represents one "slot" in the filter bar: + * - ungrouped filter → one slot with a single field + * - grouped filters → one slot for the whole group, listing all its fields + * + * Example output: + * [ + * ['fields' => ['name'], 'group' => null], // standalone filter + * ['fields' => ['dateFrom','dateTo'], 'group' => 'date'], // grouped filters + * ['fields' => ['status'], 'group' => null], // standalone filter + * ] + * + * Groups are emitted in the order their first member appears, and $processedGroups + * prevents the same group from being added a second time when the loop reaches + * subsequent members of that group. + */ + private function buildFilterLayout(): array + { + $layout = []; + $processedGroups = []; + + foreach ($this->filters as $filter) { + if (!$filter->enabled) { + continue; + } + + if ($filter->group === null) { + // Ungrouped filter: occupies its own slot. + $layout[] = ['fields' => [$filter->formFieldName], 'group' => null]; + } elseif (!in_array($filter->group, $processedGroups)) { + // First time we encounter this group: collect all enabled fields belonging + // to it (in declaration order) and emit a single slot for the whole group. + $processedGroups[] = $filter->group; + $groupFields = array_map( + fn(Filter $f) => $f->formFieldName, + array_filter($this->filters, fn(Filter $f) => $f->enabled && $f->group === $filter->group) + ); + $layout[] = ['fields' => array_values($groupFields), 'group' => $filter->group]; + } + // Subsequent members of an already-processed group are intentionally skipped. + } + + return $layout; + } + public function removeFilter(string $formFieldName): self { foreach ($this->filters as $key => $filter) { @@ -304,6 +351,7 @@ public function getGrid(bool $forceRecreate = false): Grid $this->batchMethod, $this::class, $this->rowAttributesCallback, + $this->buildFilterLayout(), ); } diff --git a/src/Resources/views/theme/bootstrap5/datagrid-filters.html.twig b/src/Resources/views/theme/bootstrap5/datagrid-filters.html.twig index 79a58ff..da620dd 100644 --- a/src/Resources/views/theme/bootstrap5/datagrid-filters.html.twig +++ b/src/Resources/views/theme/bootstrap5/datagrid-filters.html.twig @@ -1,7 +1,23 @@ {{ form_start(form) }} +{% set layoutFieldNames = [] %} +{% for row in grid.filterLayout %} + {% set layoutFieldNames = layoutFieldNames|merge(row.fields) %} + {% if row.fields|length > 1 %} +
+ {% for fieldName in row.fields %} +
+ {{ form_row(form[fieldName]) }} +
+ {% endfor %} +
+ {% else %} + {{ form_row(form[row.fields[0]]) }} + {% endif %} +{% endfor %} + {% for field in form %} - {% if field.vars.block_prefixes[0] != 'button' %} + {% if field.vars.block_prefixes[0] != 'button' and field.vars.name not in layoutFieldNames %} {{ form_row(field) }} {% endif %} {% endfor %}