From 916b681b593ef6752321f2e81ba2608f334daeb9 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 22 Aug 2025 14:24:09 +0200 Subject: [PATCH 01/13] Add column visibility group management in the datatable --- src/Column/Type/ColumnType.php | 6 ++ .../ColumnVisibilityGroup.php | 52 ++++++++++++ .../ColumnVisibilityGroupBuilder.php | 33 +++++++ .../ColumnVisibilityGroupBuilderInterface.php | 10 +++ .../ColumnVisibilityGroupInterface.php | 12 +++ src/DataTable.php | 14 +++ src/DataTableBuilder.php | 85 +++++++++++++++++++ src/DataTableBuilderInterface.php | 9 ++ src/DataTableConfigBuilder.php | 27 ++++++ src/DataTableConfigBuilderInterface.php | 3 + src/DataTableConfigInterface.php | 3 + src/DataTableInterface.php | 2 + src/DependencyInjection/Configuration.php | 3 + .../KreyuDataTableExtension.php | 1 + src/Request/HttpFoundationRequestHandler.php | 10 +++ src/Resources/config/columns.php | 7 ++ src/Type/DataTableType.php | 4 + 17 files changed, 281 insertions(+) create mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroup.php create mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php create mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilderInterface.php create mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php diff --git a/src/Column/Type/ColumnType.php b/src/Column/Type/ColumnType.php index 119f8bdc..eaf9fedd 100755 --- a/src/Column/Type/ColumnType.php +++ b/src/Column/Type/ColumnType.php @@ -330,6 +330,12 @@ public function configureOptions(OptionsResolver $resolver): void ->allowedTypes('bool') ->info('Defines whether the column can be personalized by the user in personalization feature.') ; + + $resolver->define('column_visibility_groups') + ->default(null) + ->allowedTypes('null', 'string', 'array') + ->info('Defines the visibility groups of a column.') + ; } public function getBlockPrefix(): string diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php new file mode 100644 index 00000000..de10916a --- /dev/null +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php @@ -0,0 +1,52 @@ +name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getLabel(): string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function isDefault(): bool + { + return $this->isDefault; + } + + public function setIsDefault(bool $isDefault): self + { + $this->isDefault = $isDefault; + + return $this; + } +} diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php new file mode 100644 index 00000000..0252e15c --- /dev/null +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php @@ -0,0 +1,33 @@ +configureOptions($optionResolver); + + $resolvedOptions = $optionResolver->resolve($options); + + $columnVisibilityGroup = new ColumnVisibilityGroup(); + $columnVisibilityGroup->setName($name); + $columnVisibilityGroup->setLabel($resolvedOptions['label'] ?? $name); + $columnVisibilityGroup->setIsDefault($resolvedOptions['is_default']); + + return $columnVisibilityGroup; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'label' => null, + 'is_default' => false, + ]); + } +} diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilderInterface.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilderInterface.php new file mode 100644 index 00000000..c3e44839 --- /dev/null +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilderInterface.php @@ -0,0 +1,10 @@ +personalizationData?->getColumn($column)?->isVisible() ?? $visible; } + if ($visible && $column->getConfig()->getOption('column_visibility_groups')) { + return + null === $this->columnVisibilityGroup + || in_array($this->columnVisibilityGroup, (array) $column->getConfig()->getOption('column_visibility_groups'), true); + } + return $visible; }); } @@ -880,6 +887,13 @@ public function isRequestFromTurboFrame(): bool return null !== $this->turboFrameId && 'kreyu_data_table_'.$this->getName() === $this->turboFrameId; } + public function setColumnVisibilityGroup(?string $columnVisibilityGroup): self + { + $this->columnVisibilityGroup = $columnVisibilityGroup; + + return $this; + } + private function dispatch(string $eventName, DataTableEvent $event): void { $dispatcher = $this->config->getEventDispatcher(); diff --git a/src/DataTableBuilder.php b/src/DataTableBuilder.php index 8f49c975..854b648c 100755 --- a/src/DataTableBuilder.php +++ b/src/DataTableBuilder.php @@ -13,6 +13,8 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupInterface; use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException; use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface; @@ -136,6 +138,20 @@ class DataTableBuilder extends DataTableConfigBuilder implements DataTableBuilde */ private array $unresolvedExporters = []; + /** + * The column visibility groups defined for the data table. + * + * @var array + */ + private array $columnVisibilityGroups = []; + + /** + * The data of column visibility groups that haven't been converted to column visibility group yet. + * + * @var array + */ + private array $unresolvedColumnVisibilityGroups = []; + public function __construct( string $name, ResolvedDataTableTypeInterface $type, @@ -715,6 +731,55 @@ public function removeExporter(string $name): static return $this; } + public function addColumnVisibilityGroup(ColumnVisibilityGroupInterface|string $columnVisibilityGroup, array $options = []): static + { + if ($this->locked) { + throw $this->createBuilderLockedException(); + } + + if ($columnVisibilityGroup instanceof ColumnVisibilityGroupInterface) { + $this->columns[$columnVisibilityGroup->getName()] = $columnVisibilityGroup; + + unset($this->unresolvedColumns[$columnVisibilityGroup->getName()]); + + return $this; + } + + $this->columnVisibilityGroups[$columnVisibilityGroup] = null; + $this->unresolvedColumnVisibilityGroups[$columnVisibilityGroup] = $options; + + return $this; + } + + public function hasColumnVisibilityGroup(string $name): bool + { + if ($this->locked) { + throw $this->createBuilderLockedException(); + } + + return isset($this->columnVisibilityGroups[$name]) || isset($this->unresolvedColumnVisibilityGroups[$name]); + } + + public function removeColumnVisibilityGroup(string $name): static + { + if ($this->locked) { + throw $this->createBuilderLockedException(); + } + + unset($this->unresolvedColumnVisibilityGroups[$name], $this->columnVisibilityGroups[$name]); + + return $this; + } + + public function createColumnVisibilityGroup(string $name, array $options = []): ColumnVisibilityGroupInterface + { + if ($this->locked) { + throw $this->createBuilderLockedException(); + } + + return $this->getColumnVisibilityGroupBuilder()->getColumnVisibilityGroup($name, $options); + } + public function getDataTable(): DataTableInterface { if ($this->locked) { @@ -739,6 +804,8 @@ public function getDataTable(): DataTableInterface $this->addSearchFilter(); } + $this->resolveColumnVisibilityGroups(); + $this->resolveColumns(); foreach ($this->columns as $column) { @@ -883,6 +950,24 @@ private function resolveExporters(): void } } + private function resolveColumnVisibilityGroups(): void + { + foreach (array_keys($this->unresolvedColumnVisibilityGroups) as $columnVisibilityGroups) { + $this->resolveColumnVisibilityGroup($columnVisibilityGroups); + } + } + + private function resolveColumnVisibilityGroup(string $name): ColumnVisibilityGroupInterface + { + $options = $this->unresolvedColumnVisibilityGroups[$name]; + + unset($this->unresolvedColumnVisibilityGroups[$name]); + + $columnVisibilityGroup = $this->getColumnVisibilityGroupBuilder()->getColumnVisibilityGroup($name, $options); + + return $this->columnVisibilityGroups[$name] = $columnVisibilityGroup; + } + private function shouldPrependBatchCheckboxColumn(): bool { return $this->isAutoAddingBatchCheckboxColumn() diff --git a/src/DataTableBuilderInterface.php b/src/DataTableBuilderInterface.php index 803bde4f..ff9d1797 100755 --- a/src/DataTableBuilderInterface.php +++ b/src/DataTableBuilderInterface.php @@ -9,6 +9,7 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnBuilderInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\ActionsColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupInterface; use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface; use Kreyu\Bundle\DataTableBundle\Exporter\Type\ExporterTypeInterface; @@ -196,4 +197,12 @@ public function getQuery(): ?ProxyQueryInterface; public function setQuery(?ProxyQueryInterface $query): static; public function getDataTable(): DataTableInterface; + + public function addColumnVisibilityGroup(ColumnVisibilityGroupInterface|string $columnVisibilityGroup, array $options = []): static; + + public function removeColumnVisibilityGroup(string $name): static; + + public function hasColumnVisibilityGroup(string $name): bool; + + public function createColumnVisibilityGroup(string $name, array $options = []): ColumnVisibilityGroupInterface; } diff --git a/src/DataTableConfigBuilder.php b/src/DataTableConfigBuilder.php index 4e65af54..d269ec7e 100755 --- a/src/DataTableConfigBuilder.php +++ b/src/DataTableConfigBuilder.php @@ -6,6 +6,7 @@ use Kreyu\Bundle\DataTableBundle\Action\ActionFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException; use Kreyu\Bundle\DataTableBundle\Exporter\ExportData; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface; @@ -60,6 +61,7 @@ class DataTableConfigBuilder implements DataTableConfigBuilderInterface private ?FilterFactoryInterface $filterFactory = null; private ?ActionFactoryInterface $actionFactory = null; private ?ExporterFactoryInterface $exporterFactory = null; + private ?ColumnVisibilityGroupBuilderInterface $columnVisibilityGroupBuilder = null; private ?RequestHandlerInterface $requestHandler = null; private bool $sortingClearable = false; @@ -191,6 +193,26 @@ public function setColumnFactory(ColumnFactoryInterface $columnFactory): static return $this; } + public function getColumnVisibilityGroupBuilder(): ColumnVisibilityGroupBuilderInterface + { + if (!isset($this->columnVisibilityGroupBuilder)) { + throw new BadMethodCallException(sprintf('The column factory is not set, use the "%s::setColumnVisibilityGroupBuilder()" method to set the column factory.', $this::class)); + } + + return $this->columnVisibilityGroupBuilder; + } + + public function setColumnVisibilityGroupBuilder(ColumnVisibilityGroupBuilderInterface $columnVisibilityGroupBuilder): static + { + if ($this->locked) { + throw $this->createBuilderLockedException(); + } + + $this->columnVisibilityGroupBuilder = $columnVisibilityGroupBuilder; + + return $this; + } + public function getFilterFactory(): FilterFactoryInterface { if (!isset($this->filterFactory)) { @@ -850,6 +872,11 @@ public function getPersonalizationParameterName(): string return $this->getParameterName(static::PERSONALIZATION_PARAMETER); } + public function getColumnVisibilityGroupParameterName(): string + { + return $this->getParameterName(static::COLUMN_VISIBILITY_GROUP_PARAMETER); + } + public function getExportParameterName(): string { return $this->getParameterName(static::EXPORT_PARAMETER); diff --git a/src/DataTableConfigBuilderInterface.php b/src/DataTableConfigBuilderInterface.php index 064499b1..66207850 100755 --- a/src/DataTableConfigBuilderInterface.php +++ b/src/DataTableConfigBuilderInterface.php @@ -6,6 +6,7 @@ use Kreyu\Bundle\DataTableBundle\Action\ActionFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; use Kreyu\Bundle\DataTableBundle\Exporter\ExportData; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface; use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface; @@ -51,6 +52,8 @@ public function setActionFactory(ActionFactoryInterface $actionFactory): static; public function setExporterFactory(ExporterFactoryInterface $exporterFactory): static; + public function setColumnVisibilityGroupBuilder(ColumnVisibilityGroupBuilderInterface $columnVisibilityGroupBuilder): static; + public function setExportingEnabled(bool $exportingEnabled): static; public function setExportFormFactory(?FormFactoryInterface $exportFormFactory): static; diff --git a/src/DataTableConfigInterface.php b/src/DataTableConfigInterface.php index c78eba6a..374ed1da 100755 --- a/src/DataTableConfigInterface.php +++ b/src/DataTableConfigInterface.php @@ -27,6 +27,7 @@ interface DataTableConfigInterface public const SORT_PARAMETER = 'sort'; public const FILTRATION_PARAMETER = 'filter'; public const PERSONALIZATION_PARAMETER = 'personalization'; + public const COLUMN_VISIBILITY_GROUP_PARAMETER = 'column_visibility_group'; public const EXPORT_PARAMETER = 'export'; public function getEventDispatcher(): EventDispatcherInterface; @@ -133,5 +134,7 @@ public function getFiltrationParameterName(): string; public function getPersonalizationParameterName(): string; + public function getColumnVisibilityGroupParameterName(): string; + public function getExportParameterName(): string; } diff --git a/src/DataTableInterface.php b/src/DataTableInterface.php index 177bb90e..b9d4881e 100755 --- a/src/DataTableInterface.php +++ b/src/DataTableInterface.php @@ -219,4 +219,6 @@ public function createExportView(): DataTableView; public function setTurboFrameId(string $turboFrameId): static; public function isRequestFromTurboFrame(): bool; + + public function setColumnVisibilityGroup(?string $columnVisibilityGroup): self; } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index e1ec2e20..571cda97 100755 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -48,6 +48,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('action_factory') ->defaultValue('kreyu_data_table.action.factory') ->end() + ->scalarNode('column_visibility_group_builder') + ->defaultValue('kreyu_data_table.column_visibility_group.builder') + ->end() ->scalarNode('request_handler') ->defaultValue('kreyu_data_table.request_handler.http_foundation') ->end() diff --git a/src/DependencyInjection/KreyuDataTableExtension.php b/src/DependencyInjection/KreyuDataTableExtension.php index 57b9ddec..06b756d4 100755 --- a/src/DependencyInjection/KreyuDataTableExtension.php +++ b/src/DependencyInjection/KreyuDataTableExtension.php @@ -116,6 +116,7 @@ private function resolveConfiguration(array $configs, ContainerBuilder $containe 'action_factory', 'filter_factory', 'exporter_factory', + 'column_visibility_group_builder', 'request_handler', ]; diff --git a/src/Request/HttpFoundationRequestHandler.php b/src/Request/HttpFoundationRequestHandler.php index 29c9efbe..f12c76a2 100755 --- a/src/Request/HttpFoundationRequestHandler.php +++ b/src/Request/HttpFoundationRequestHandler.php @@ -38,6 +38,7 @@ public function handle(DataTableInterface $dataTable, mixed $request = null): vo $this->paginate($dataTable, $request); $this->export($dataTable, $request); $this->turbo($dataTable, $request); + $this->columnVisibilityGroup($dataTable, $request); } private function filter(DataTableInterface $dataTable, Request $request): void @@ -140,4 +141,13 @@ private function turbo(DataTableInterface $dataTable, Request $request): void { $dataTable->setTurboFrameId($request->headers->get('Turbo-Frame')); } + + private function columnVisibilityGroup(DataTableInterface $dataTable, Request $request): void + { + $parameterName = $dataTable->getConfig()->getColumnVisibilityGroupParameterName(); + + $columnVisibility = $this->extractQueryParameter($request, "[$parameterName]"); + + $dataTable->setColumnVisibilityGroup($columnVisibility); + } } diff --git a/src/Resources/config/columns.php b/src/Resources/config/columns.php index 46e9ffe5..c6d214cd 100755 --- a/src/Resources/config/columns.php +++ b/src/Resources/config/columns.php @@ -26,6 +26,8 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TemplateColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilder; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -56,6 +58,11 @@ ->alias(ColumnFactoryInterface::class, 'kreyu_data_table.column.factory') ; + $services + ->set('kreyu_data_table.column_visibility_group.builder', ColumnVisibilityGroupBuilder::class) + ->alias(ColumnVisibilityGroupBuilderInterface::class, 'kreyu_data_table.column_visibility_group.builder') + ; + $services ->set('kreyu_data_table.column.type.column', ColumnType::class) ->args([service('translator')->nullOnInvalid()]) diff --git a/src/Type/DataTableType.php b/src/Type/DataTableType.php index e76e935a..112f2c3e 100755 --- a/src/Type/DataTableType.php +++ b/src/Type/DataTableType.php @@ -9,6 +9,7 @@ use Kreyu\Bundle\DataTableBundle\Action\ActionView; use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\DataTableInterface; use Kreyu\Bundle\DataTableBundle\DataTableView; @@ -43,6 +44,7 @@ public function buildDataTable(DataTableBuilderInterface $builder, array $option $setters = [ 'themes' => $builder->setThemes(...), 'column_factory' => $builder->setColumnFactory(...), + 'column_visibility_group_builder' => $builder->setColumnVisibilityGroupBuilder(...), 'filter_factory' => $builder->setFilterFactory(...), 'action_factory' => $builder->setActionFactory(...), 'exporter_factory' => $builder->setExporterFactory(...), @@ -163,6 +165,7 @@ public function configureOptions(OptionsResolver $resolver): void 'filter_factory' => $this->defaults['filtration']['filter_factory'] ?? null, 'action_factory' => $this->defaults['action_factory'] ?? null, 'exporter_factory' => $this->defaults['exporting']['exporter_factory'] ?? null, + 'column_visibility_group_builder' => $this->defaults['column_visibility_group_builder'] ?? null, 'request_handler' => $this->defaults['request_handler'] ?? null, 'sorting_enabled' => $this->defaults['sorting']['enabled'] ?? true, 'sorting_clearable' => $this->defaults['sorting']['clearable'] ?? true, @@ -195,6 +198,7 @@ public function configureOptions(OptionsResolver $resolver): void ->setAllowedTypes('filter_factory', ['null', FilterFactoryInterface::class]) ->setAllowedTypes('action_factory', ['null', ActionFactoryInterface::class]) ->setAllowedTypes('exporter_factory', ['null', ExporterFactoryInterface::class]) + ->setAllowedTypes('column_visibility_group_builder', ['null', ColumnVisibilityGroupBuilderInterface::class]) ->setAllowedTypes('request_handler', ['null', RequestHandlerInterface::class]) ->setAllowedTypes('sorting_enabled', 'bool') ->setAllowedTypes('sorting_clearable', 'bool') From 1a5203bf16f4c52868d975a1423774cbd2ecc22b Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 22 Aug 2025 15:32:21 +0200 Subject: [PATCH 02/13] Enhance column visibility group management with translation support and UI integration --- .../ColumnVisibilityGroupBuilder.php | 8 +++++- .../ColumnVisibilityGroupInterface.php | 1 + src/DataTable.php | 28 +++++++++++++++---- src/DataTableBuilder.php | 10 +++++-- src/DataTableInterface.php | 7 ++++- src/DataTableView.php | 6 ++++ src/Request/HttpFoundationRequestHandler.php | 12 ++++++-- src/Resources/config/columns.php | 1 + src/Resources/views/themes/base.html.twig | 25 ++++++++++++++++- .../views/themes/bootstrap_5.html.twig | 8 ++++++ src/Type/DataTableType.php | 2 ++ 11 files changed, 94 insertions(+), 14 deletions(-) diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php index 0252e15c..d6cfd9f6 100644 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php @@ -5,9 +5,15 @@ namespace Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; class ColumnVisibilityGroupBuilder implements ColumnVisibilityGroupBuilderInterface { + public function __construct( + private TranslatorInterface $translator, + ) { + } + public function getColumnVisibilityGroup(string $name, array $options = []): ColumnVisibilityGroupInterface { $optionResolver = new OptionsResolver(); @@ -17,7 +23,7 @@ public function getColumnVisibilityGroup(string $name, array $options = []): Col $columnVisibilityGroup = new ColumnVisibilityGroup(); $columnVisibilityGroup->setName($name); - $columnVisibilityGroup->setLabel($resolvedOptions['label'] ?? $name); + $columnVisibilityGroup->setLabel($this->translator->trans($resolvedOptions['label'] ?? $name)); $columnVisibilityGroup->setIsDefault($resolvedOptions['is_default']); return $columnVisibilityGroup; diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php index 9a55fe04..da76d67e 100644 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php @@ -9,4 +9,5 @@ interface ColumnVisibilityGroupInterface public function getName(): string; public function getLabel(): string; public function isDefault(): bool; + public function setIsDefault(bool $isDefault): self; } diff --git a/src/DataTable.php b/src/DataTable.php index 19fca31b..9ec57aab 100755 --- a/src/DataTable.php +++ b/src/DataTable.php @@ -9,6 +9,7 @@ use Kreyu\Bundle\DataTableBundle\Action\Type\ActionType; use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnType; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupInterface; use Kreyu\Bundle\DataTableBundle\Event\DataTableEvent; use Kreyu\Bundle\DataTableBundle\Event\DataTableEvents; use Kreyu\Bundle\DataTableBundle\Event\DataTableExportEvent; @@ -74,6 +75,11 @@ class DataTable implements DataTableInterface */ private array $exporters = []; + /** + * @var array + */ + private array $columnVisibilityGroups = []; + /** * The sorting data currently applied to the data table. */ @@ -113,7 +119,7 @@ class DataTable implements DataTableInterface private bool $initialized = false; private ?string $turboFrameId = null; - private ?string $columnVisibilityGroup = null; + private ?string $requestedColumnVisibilityGroup = null; public function __construct( private ProxyQueryInterface $query, @@ -205,8 +211,8 @@ public function getVisibleColumns(): array if ($visible && $column->getConfig()->getOption('column_visibility_groups')) { return - null === $this->columnVisibilityGroup - || in_array($this->columnVisibilityGroup, (array) $column->getConfig()->getOption('column_visibility_groups'), true); + null === $this->requestedColumnVisibilityGroup + || in_array($this->requestedColumnVisibilityGroup, (array) $column->getConfig()->getOption('column_visibility_groups'), true); } return $visible; @@ -515,6 +521,18 @@ public function removeExporter(string $name): static return $this; } + public function addColumnVisibilityGroup(ColumnVisibilityGroupInterface $columnVisibilityGroup): static + { + $this->columnVisibilityGroups[$columnVisibilityGroup->getName()] = $columnVisibilityGroup; + + return $this; + } + + public function getColumnVisibilityGroups(): array + { + return $this->columnVisibilityGroups; + } + public function paginate(PaginationData $data, bool $persistence = true): void { if (!$this->config->isPaginationEnabled()) { @@ -887,9 +905,9 @@ public function isRequestFromTurboFrame(): bool return null !== $this->turboFrameId && 'kreyu_data_table_'.$this->getName() === $this->turboFrameId; } - public function setColumnVisibilityGroup(?string $columnVisibilityGroup): self + public function setRequestedColumnVisibilityGroup(?string $requestedColumnVisibilityGroup): self { - $this->columnVisibilityGroup = $columnVisibilityGroup; + $this->requestedColumnVisibilityGroup = $requestedColumnVisibilityGroup; return $this; } diff --git a/src/DataTableBuilder.php b/src/DataTableBuilder.php index 854b648c..588ac969 100755 --- a/src/DataTableBuilder.php +++ b/src/DataTableBuilder.php @@ -141,7 +141,7 @@ class DataTableBuilder extends DataTableConfigBuilder implements DataTableBuilde /** * The column visibility groups defined for the data table. * - * @var array + * @var array */ private array $columnVisibilityGroups = []; @@ -804,14 +804,18 @@ public function getDataTable(): DataTableInterface $this->addSearchFilter(); } - $this->resolveColumnVisibilityGroups(); - $this->resolveColumns(); foreach ($this->columns as $column) { $dataTable->addColumn($column->getColumn()); } + $this->resolveColumnVisibilityGroups(); + + foreach ($this->columnVisibilityGroups as $columnVisibilityGroup) { + $dataTable->addColumnVisibilityGroup($columnVisibilityGroup); + } + $this->resolveFilters(); foreach ($this->filters as $filter) { diff --git a/src/DataTableInterface.php b/src/DataTableInterface.php index b9d4881e..e29a17bf 100755 --- a/src/DataTableInterface.php +++ b/src/DataTableInterface.php @@ -10,6 +10,7 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupInterface; use Kreyu\Bundle\DataTableBundle\Exception\OutOfBoundsException; use Kreyu\Bundle\DataTableBundle\Exporter\ExportData; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterInterface; @@ -219,6 +220,10 @@ public function createExportView(): DataTableView; public function setTurboFrameId(string $turboFrameId): static; public function isRequestFromTurboFrame(): bool; + public function setRequestedColumnVisibilityGroup(?string $requestedColumnVisibilityGroup): self; - public function setColumnVisibilityGroup(?string $columnVisibilityGroup): self; + /** + * @return array + */ + public function getColumnVisibilityGroups(): array; } diff --git a/src/DataTableView.php b/src/DataTableView.php index 2fa5ea09..b98c0542 100755 --- a/src/DataTableView.php +++ b/src/DataTableView.php @@ -5,6 +5,7 @@ namespace Kreyu\Bundle\DataTableBundle; use Kreyu\Bundle\DataTableBundle\Action\ActionView; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupInterface; use Kreyu\Bundle\DataTableBundle\Filter\FilterView; use Kreyu\Bundle\DataTableBundle\Pagination\PaginationView; @@ -32,4 +33,9 @@ class DataTableView * @var array */ public array $actions = []; + + /** + * @var array + */ + public array $columnVisibilityGroups = []; } diff --git a/src/Request/HttpFoundationRequestHandler.php b/src/Request/HttpFoundationRequestHandler.php index f12c76a2..fb786d6e 100755 --- a/src/Request/HttpFoundationRequestHandler.php +++ b/src/Request/HttpFoundationRequestHandler.php @@ -146,8 +146,14 @@ private function columnVisibilityGroup(DataTableInterface $dataTable, Request $r { $parameterName = $dataTable->getConfig()->getColumnVisibilityGroupParameterName(); - $columnVisibility = $this->extractQueryParameter($request, "[$parameterName]"); - - $dataTable->setColumnVisibilityGroup($columnVisibility); + $requestColumnVisibilityGroup = $this->extractQueryParameter($request, "[$parameterName]"); + + // This also checks if the requested column visibility group exists + foreach ($dataTable->getColumnVisibilityGroups() as $columnVisibilityGroup) { + if ($columnVisibilityGroup->getName() === $requestColumnVisibilityGroup) { + $columnVisibilityGroup->setIsDefault(true); + $dataTable->setRequestedColumnVisibilityGroup($requestColumnVisibilityGroup); + } + } } } diff --git a/src/Resources/config/columns.php b/src/Resources/config/columns.php index c6d214cd..6711b833 100755 --- a/src/Resources/config/columns.php +++ b/src/Resources/config/columns.php @@ -60,6 +60,7 @@ $services ->set('kreyu_data_table.column_visibility_group.builder', ColumnVisibilityGroupBuilder::class) + ->args([service('translator')]) ->alias(ColumnVisibilityGroupBuilderInterface::class, 'kreyu_data_table.column_visibility_group.builder') ; diff --git a/src/Resources/views/themes/base.html.twig b/src/Resources/views/themes/base.html.twig index 53dcf297..31cbb5f7 100755 --- a/src/Resources/views/themes/base.html.twig +++ b/src/Resources/views/themes/base.html.twig @@ -59,7 +59,30 @@ {{ block('action_bar', theme) }} {% endblock %} -{% block action_bar %}{% endblock %} +{% block action_bar %} + {{ block('column_visibility_group_selector', theme) }} +{% endblock %} + +{% block column_visibility_group_selector %} + {% if data_table.columnVisibilityGroups is not empty %} + + {% set form_attr = { 'data-turbo-frame': '_self' }|merge(form_attr ?? {}) %} + +
+ {% set select_attr = { + name: data_table.vars.column_visibility_group_parameter_name, + autocomplete: 'off', + onchange: 'this.form.requestSubmit()', + }|merge(select_attr|default({})) %} + + +
+ {% endif %} +{% endblock %} {% block table %} diff --git a/src/Resources/views/themes/bootstrap_5.html.twig b/src/Resources/views/themes/bootstrap_5.html.twig index 59b64b0a..05217a91 100755 --- a/src/Resources/views/themes/bootstrap_5.html.twig +++ b/src/Resources/views/themes/bootstrap_5.html.twig @@ -36,6 +36,8 @@
+ {{ block('column_visibility_group_selector', theme) }} + {% if filtration_enabled and filtration_form and filtration_form.vars.search_fields|length > 0 %}
{{ block('action_search') }} @@ -492,6 +494,12 @@ {% endwith %} {% endblock %} +{% block column_visibility_group_selector %} + {% with { select_attr: { class: 'col form-select' } } %} + {{ parent() }} + {% endwith %} +{% endblock %} + {% block pagination_page %}
{% with { attr: { class: 'page-link' } } %} diff --git a/src/Type/DataTableType.php b/src/Type/DataTableType.php index 112f2c3e..62cb4b1f 100755 --- a/src/Type/DataTableType.php +++ b/src/Type/DataTableType.php @@ -103,6 +103,7 @@ public function buildView(DataTableView $view, DataTableInterface $dataTable, ar 'filtration_parameter_name' => $dataTable->getConfig()->getFiltrationParameterName(), 'personalization_parameter_name' => $dataTable->getConfig()->getPersonalizationParameterName(), 'export_parameter_name' => $dataTable->getConfig()->getExportParameterName(), + 'column_visibility_group_parameter_name' => $dataTable->getConfig()->getColumnVisibilityGroupParameterName(), 'has_active_filters' => $dataTable->hasActiveFilters(), 'filtration_data' => $dataTable->getFiltrationData(), 'sorting_data' => $dataTable->getSortingData(), @@ -117,6 +118,7 @@ public function buildView(DataTableView $view, DataTableInterface $dataTable, ar $view->pagination = $this->createPaginationView($view, $dataTable); $view->filters = $this->createFilterViews($view, $dataTable); $view->actions = $this->createActionViews($view, $dataTable); + $view->columnVisibilityGroups = $dataTable->getColumnVisibilityGroups(); $view->vars = array_replace($view->vars, [ 'header_row' => $view->headerRow, From 47dea7dd49566e1aaec89bd6318189b2cfeb96e0 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 22 Aug 2025 15:47:52 +0200 Subject: [PATCH 03/13] Add documentation --- .../docs/features/column-visibility-group.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/src/docs/features/column-visibility-group.md diff --git a/docs/src/docs/features/column-visibility-group.md b/docs/src/docs/features/column-visibility-group.md new file mode 100644 index 00000000..9e19cd92 --- /dev/null +++ b/docs/src/docs/features/column-visibility-group.md @@ -0,0 +1,65 @@ +# Column Visibility Groups + +Column Visibility Groups allow you to organize table columns into different "views." This is useful when you have a lot of information to display in a single row and want to separate it into multiple, easily switchable groups. Users can select which group of columns to display using a dropdown in the table UI. + +[[toc]] + +## Basic Usage + +By default, a data table has a single visibility group. You can define additional groups and assign columns to them. + +```php +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; +use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; +use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; +use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; + +class ExampleDataTableType extends AbstractDataTableType +{ + public function buildDataTable(DataTableBuilderInterface $builder, array $options): void + { + // Define visibility groups + $builder->addColumnVisibilityGroup('default'); + $builder->addColumnVisibilityGroup('address', [ + // By default, the group label is the group name, but you can override it: + 'label' => 'Address related content', + // By default, the first defined group is the default one, but you can override it: + 'is_default' => true, + ]); + + // Assign groups to columns + $builder + ->addColumn('id', NumberColumnType::class, [ + 'sort' => true, + // Will always be displayed as it does not have any group assigned + ]) + ->addColumn('name', TextColumnType::class, [ + 'label' => 'Full name', + 'sort' => true, + ]) + ->addColumn('streetName', TextColumnType::class, [ + 'sort' => true, + // This column will only be visible when the "address" group is selected + 'column_visibility_groups' => ['address'], + ]) + ; + } +} +``` + +## How It Works + +- **Defining Groups:** Use `$builder->addColumnVisibilityGroup($name, $options)` to define one or more groups. The `label` option is used as the display name in the UI. +- **Assigning Columns:** Use the `column_visibility_groups` option in `addColumn()` to assign a column to one or more groups. If omitted or set to `null`/`[]`, the column will always be visible. +- **Switching Views:** A select dropdown appears in the table, allowing users to switch between the different column visibility groups. + +## Notes + +- You can define as many visibility groups as needed. +- A column can belong to multiple groups by specifying multiple group names in the `column_visibility_groups` array. +- If `column_visibility_groups` is `null` or an empty array, the column is shown in the "default" group. +- Creating a default group is optional but recommended for better user experience : it ensures that the user can go back to the base view. + +## UI + +When multiple visibility groups are present, a select dropdown is rendered above the table, allowing users to choose which group of columns to display. From ca34083b6196edd9c27a8ab16f1e85bd0f4c0c2d Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 22 Aug 2025 15:48:03 +0200 Subject: [PATCH 04/13] Run php-cs-fixer --- src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php | 3 +++ src/DataTableBuilder.php | 1 - src/DataTableInterface.php | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php index da76d67e..eb43ff68 100644 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php @@ -7,7 +7,10 @@ interface ColumnVisibilityGroupInterface { public function getName(): string; + public function getLabel(): string; + public function isDefault(): bool; + public function setIsDefault(bool $isDefault): self; } diff --git a/src/DataTableBuilder.php b/src/DataTableBuilder.php index 588ac969..e3fc0f2b 100755 --- a/src/DataTableBuilder.php +++ b/src/DataTableBuilder.php @@ -13,7 +13,6 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\CheckboxColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\ColumnTypeInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; -use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupInterface; use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException; use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; diff --git a/src/DataTableInterface.php b/src/DataTableInterface.php index e29a17bf..9e2cc0c4 100755 --- a/src/DataTableInterface.php +++ b/src/DataTableInterface.php @@ -220,6 +220,7 @@ public function createExportView(): DataTableView; public function setTurboFrameId(string $turboFrameId): static; public function isRequestFromTurboFrame(): bool; + public function setRequestedColumnVisibilityGroup(?string $requestedColumnVisibilityGroup): self; /** From 4bdb339f83adec4213f7bfe928d1a6b54fcf3206 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 22 Aug 2025 17:08:23 +0200 Subject: [PATCH 05/13] Update the URL with the visibility group + handle properly default values --- src/Request/HttpFoundationRequestHandler.php | 5 +++++ src/Resources/views/themes/base.html.twig | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Request/HttpFoundationRequestHandler.php b/src/Request/HttpFoundationRequestHandler.php index fb786d6e..a924ec20 100755 --- a/src/Request/HttpFoundationRequestHandler.php +++ b/src/Request/HttpFoundationRequestHandler.php @@ -148,8 +148,13 @@ private function columnVisibilityGroup(DataTableInterface $dataTable, Request $r $requestColumnVisibilityGroup = $this->extractQueryParameter($request, "[$parameterName]"); + if (null === $requestColumnVisibilityGroup) { + return; + } + // This also checks if the requested column visibility group exists foreach ($dataTable->getColumnVisibilityGroups() as $columnVisibilityGroup) { + $columnVisibilityGroup->setIsDefault(false); if ($columnVisibilityGroup->getName() === $requestColumnVisibilityGroup) { $columnVisibilityGroup->setIsDefault(true); $dataTable->setRequestedColumnVisibilityGroup($requestColumnVisibilityGroup); diff --git a/src/Resources/views/themes/base.html.twig b/src/Resources/views/themes/base.html.twig index 31cbb5f7..605274e7 100755 --- a/src/Resources/views/themes/base.html.twig +++ b/src/Resources/views/themes/base.html.twig @@ -66,7 +66,7 @@ {% block column_visibility_group_selector %} {% if data_table.columnVisibilityGroups is not empty %} - {% set form_attr = { 'data-turbo-frame': '_self' }|merge(form_attr ?? {}) %} + {% set form_attr = { 'data-turbo-frame': '_self', 'data-turbo-action': 'advance' }|merge(form_attr ?? {}) %}
{% set select_attr = { From 895a374b7eafc7e5894b1bafd1a76315c3b4be72 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Mon, 12 Jan 2026 15:00:43 +0100 Subject: [PATCH 06/13] Use "selected" instead of "default" --- .../ColumnVisibilityGroup.php | 14 +++++++++++++- .../ColumnVisibilityGroupInterface.php | 4 ++++ src/Request/HttpFoundationRequestHandler.php | 14 ++++++++++---- src/Resources/views/themes/base.html.twig | 2 +- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php index de10916a..979b35a9 100644 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php @@ -8,7 +8,8 @@ class ColumnVisibilityGroup implements ColumnVisibilityGroupInterface { private string $name; private string $label; - private bool $isDefault; + private bool $isDefault = false; + private bool $isSelected = false; public function __construct() { @@ -49,4 +50,15 @@ public function setIsDefault(bool $isDefault): self return $this; } + + public function isSelected(): bool + { + return $this->isSelected; + } + + public function setIsSelected(bool $isSelected): self + { + $this->isSelected = $isSelected; + return $this; + } } diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php index eb43ff68..aa58fcc2 100644 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroupInterface.php @@ -13,4 +13,8 @@ public function getLabel(): string; public function isDefault(): bool; public function setIsDefault(bool $isDefault): self; + + public function isSelected(): bool; + + public function setIsSelected(bool $isSelected): self; } diff --git a/src/Request/HttpFoundationRequestHandler.php b/src/Request/HttpFoundationRequestHandler.php index a924ec20..77167fc5 100755 --- a/src/Request/HttpFoundationRequestHandler.php +++ b/src/Request/HttpFoundationRequestHandler.php @@ -148,15 +148,21 @@ private function columnVisibilityGroup(DataTableInterface $dataTable, Request $r $requestColumnVisibilityGroup = $this->extractQueryParameter($request, "[$parameterName]"); + $mustBeDefault = false; if (null === $requestColumnVisibilityGroup) { - return; + $mustBeDefault = true; } // This also checks if the requested column visibility group exists foreach ($dataTable->getColumnVisibilityGroups() as $columnVisibilityGroup) { - $columnVisibilityGroup->setIsDefault(false); - if ($columnVisibilityGroup->getName() === $requestColumnVisibilityGroup) { - $columnVisibilityGroup->setIsDefault(true); + if ( + ($mustBeDefault && $columnVisibilityGroup->isDefault()) + || $columnVisibilityGroup->getName() === $requestColumnVisibilityGroup + ) { + $columnVisibilityGroup->setIsSelected(true); + if ($mustBeDefault) { + $requestColumnVisibilityGroup = $columnVisibilityGroup->getName(); + } $dataTable->setRequestedColumnVisibilityGroup($requestColumnVisibilityGroup); } } diff --git a/src/Resources/views/themes/base.html.twig b/src/Resources/views/themes/base.html.twig index 605274e7..57300c4f 100755 --- a/src/Resources/views/themes/base.html.twig +++ b/src/Resources/views/themes/base.html.twig @@ -77,7 +77,7 @@ From f9b5761c2131fc33a50e35973d53f57f3b05f9d3 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Mon, 12 Jan 2026 15:02:20 +0100 Subject: [PATCH 07/13] PHPCSFixer --- src/ColumnVisibilityGroup/ColumnVisibilityGroup.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php index 979b35a9..6ed9d51e 100644 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php @@ -59,6 +59,7 @@ public function isSelected(): bool public function setIsSelected(bool $isSelected): self { $this->isSelected = $isSelected; + return $this; } } From 4fd24ffbde87ff32b73097b2256026c66001bdd6 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Thu, 16 Apr 2026 22:52:17 +0200 Subject: [PATCH 08/13] Fix column visibility group implementation --- .../docs/features/column-visibility-group.md | 2 +- .../ColumnVisibilityGroup.php | 45 +++---------------- .../ColumnVisibilityGroupBuilder.php | 39 ---------------- .../ColumnVisibilityGroupBuilderInterface.php | 10 ----- .../ColumnVisibilityGroupFactory.php | 41 +++++++++++++++++ .../ColumnVisibilityGroupFactoryInterface.php | 10 +++++ .../ColumnVisibilityGroupInterface.php | 6 --- .../ColumnVisibilityGroupView.php | 16 +++++++ src/DataTable.php | 15 +++++-- src/DataTableBuilder.php | 21 ++++++--- src/DataTableConfigBuilder.php | 16 +++---- src/DataTableConfigBuilderInterface.php | 4 +- src/DataTableConfigInterface.php | 3 ++ src/DataTableInterface.php | 4 ++ src/DataTableView.php | 4 +- src/DependencyInjection/Configuration.php | 4 +- .../KreyuDataTableExtension.php | 2 +- src/Request/HttpFoundationRequestHandler.php | 32 ++++++------- src/Resources/config/columns.php | 8 ++-- src/Type/DataTableType.php | 38 +++++++++++++--- 20 files changed, 175 insertions(+), 145 deletions(-) delete mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php delete mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilderInterface.php create mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroupFactory.php create mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroupFactoryInterface.php create mode 100644 src/ColumnVisibilityGroup/ColumnVisibilityGroupView.php diff --git a/docs/src/docs/features/column-visibility-group.md b/docs/src/docs/features/column-visibility-group.md index 9e19cd92..ef0b2f79 100644 --- a/docs/src/docs/features/column-visibility-group.md +++ b/docs/src/docs/features/column-visibility-group.md @@ -57,7 +57,7 @@ class ExampleDataTableType extends AbstractDataTableType - You can define as many visibility groups as needed. - A column can belong to multiple groups by specifying multiple group names in the `column_visibility_groups` array. -- If `column_visibility_groups` is `null` or an empty array, the column is shown in the "default" group. +- If `column_visibility_groups` is `null` or an empty array, the column is visible in every group (it does not belong to any group and is therefore never filtered out). - Creating a default group is optional but recommended for better user experience : it ensures that the user can go back to the base view. ## UI diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php index 6ed9d51e..714a522b 100644 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroup.php @@ -6,13 +6,11 @@ class ColumnVisibilityGroup implements ColumnVisibilityGroupInterface { - private string $name; - private string $label; - private bool $isDefault = false; - private bool $isSelected = false; - - public function __construct() - { + public function __construct( + private readonly string $name, + private readonly string $label, + private readonly bool $isDefault = false, + ) { } public function getName(): string @@ -20,46 +18,13 @@ public function getName(): string return $this->name; } - public function setName(string $name): self - { - $this->name = $name; - - return $this; - } - public function getLabel(): string { return $this->label; } - public function setLabel(string $label): self - { - $this->label = $label; - - return $this; - } - public function isDefault(): bool { return $this->isDefault; } - - public function setIsDefault(bool $isDefault): self - { - $this->isDefault = $isDefault; - - return $this; - } - - public function isSelected(): bool - { - return $this->isSelected; - } - - public function setIsSelected(bool $isSelected): self - { - $this->isSelected = $isSelected; - - return $this; - } } diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php deleted file mode 100644 index d6cfd9f6..00000000 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilder.php +++ /dev/null @@ -1,39 +0,0 @@ -configureOptions($optionResolver); - - $resolvedOptions = $optionResolver->resolve($options); - - $columnVisibilityGroup = new ColumnVisibilityGroup(); - $columnVisibilityGroup->setName($name); - $columnVisibilityGroup->setLabel($this->translator->trans($resolvedOptions['label'] ?? $name)); - $columnVisibilityGroup->setIsDefault($resolvedOptions['is_default']); - - return $columnVisibilityGroup; - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'label' => null, - 'is_default' => false, - ]); - } -} diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilderInterface.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilderInterface.php deleted file mode 100644 index c3e44839..00000000 --- a/src/ColumnVisibilityGroup/ColumnVisibilityGroupBuilderInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -configureOptions($optionsResolver); + + $resolvedOptions = $optionsResolver->resolve($options); + + return new ColumnVisibilityGroup( + name: $name, + label: $this->translator->trans($resolvedOptions['label'] ?? $name), + isDefault: $resolvedOptions['is_default'], + ); + } + + private function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'label' => null, + 'is_default' => false, + ]); + + $resolver->setAllowedTypes('label', ['null', 'string']); + $resolver->setAllowedTypes('is_default', 'bool'); + } +} diff --git a/src/ColumnVisibilityGroup/ColumnVisibilityGroupFactoryInterface.php b/src/ColumnVisibilityGroup/ColumnVisibilityGroupFactoryInterface.php new file mode 100644 index 00000000..df5a4179 --- /dev/null +++ b/src/ColumnVisibilityGroup/ColumnVisibilityGroupFactoryInterface.php @@ -0,0 +1,10 @@ +personalizationData?->getColumn($column)?->isVisible() ?? $visible; } - if ($visible && $column->getConfig()->getOption('column_visibility_groups')) { - return - null === $this->requestedColumnVisibilityGroup - || in_array($this->requestedColumnVisibilityGroup, (array) $column->getConfig()->getOption('column_visibility_groups'), true); + if ($visible && null !== $this->requestedColumnVisibilityGroup) { + $groups = (array) $column->getConfig()->getOption('column_visibility_groups'); + + if (!empty($groups)) { + $visible = in_array($this->requestedColumnVisibilityGroup, $groups, true); + } } return $visible; @@ -912,6 +914,11 @@ public function setRequestedColumnVisibilityGroup(?string $requestedColumnVisibi return $this; } + public function getRequestedColumnVisibilityGroup(): ?string + { + return $this->requestedColumnVisibilityGroup; + } + private function dispatch(string $eventName, DataTableEvent $event): void { $dispatcher = $this->config->getEventDispatcher(); diff --git a/src/DataTableBuilder.php b/src/DataTableBuilder.php index e3fc0f2b..43dcc5ed 100755 --- a/src/DataTableBuilder.php +++ b/src/DataTableBuilder.php @@ -737,9 +737,9 @@ public function addColumnVisibilityGroup(ColumnVisibilityGroupInterface|string $ } if ($columnVisibilityGroup instanceof ColumnVisibilityGroupInterface) { - $this->columns[$columnVisibilityGroup->getName()] = $columnVisibilityGroup; + $this->columnVisibilityGroups[$columnVisibilityGroup->getName()] = $columnVisibilityGroup; - unset($this->unresolvedColumns[$columnVisibilityGroup->getName()]); + unset($this->unresolvedColumnVisibilityGroups[$columnVisibilityGroup->getName()]); return $this; } @@ -776,7 +776,7 @@ public function createColumnVisibilityGroup(string $name, array $options = []): throw $this->createBuilderLockedException(); } - return $this->getColumnVisibilityGroupBuilder()->getColumnVisibilityGroup($name, $options); + return $this->getColumnVisibilityGroupFactory()->create($name, $options); } public function getDataTable(): DataTableInterface @@ -955,8 +955,17 @@ private function resolveExporters(): void private function resolveColumnVisibilityGroups(): void { - foreach (array_keys($this->unresolvedColumnVisibilityGroups) as $columnVisibilityGroups) { - $this->resolveColumnVisibilityGroup($columnVisibilityGroups); + foreach (array_keys($this->unresolvedColumnVisibilityGroups) as $name) { + $this->resolveColumnVisibilityGroup($name); + } + + $defaults = array_filter( + $this->columnVisibilityGroups, + static fn (ColumnVisibilityGroupInterface $group) => $group->isDefault(), + ); + + if (count($defaults) > 1) { + throw new InvalidArgumentException(sprintf('Only one column visibility group can be marked as default, but %d were found: "%s".', count($defaults), implode('", "', array_keys($defaults)))); } } @@ -966,7 +975,7 @@ private function resolveColumnVisibilityGroup(string $name): ColumnVisibilityGro unset($this->unresolvedColumnVisibilityGroups[$name]); - $columnVisibilityGroup = $this->getColumnVisibilityGroupBuilder()->getColumnVisibilityGroup($name, $options); + $columnVisibilityGroup = $this->getColumnVisibilityGroupFactory()->create($name, $options); return $this->columnVisibilityGroups[$name] = $columnVisibilityGroup; } diff --git a/src/DataTableConfigBuilder.php b/src/DataTableConfigBuilder.php index d269ec7e..270b40bd 100755 --- a/src/DataTableConfigBuilder.php +++ b/src/DataTableConfigBuilder.php @@ -6,7 +6,7 @@ use Kreyu\Bundle\DataTableBundle\Action\ActionFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface; -use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactoryInterface; use Kreyu\Bundle\DataTableBundle\Exception\BadMethodCallException; use Kreyu\Bundle\DataTableBundle\Exporter\ExportData; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface; @@ -61,7 +61,7 @@ class DataTableConfigBuilder implements DataTableConfigBuilderInterface private ?FilterFactoryInterface $filterFactory = null; private ?ActionFactoryInterface $actionFactory = null; private ?ExporterFactoryInterface $exporterFactory = null; - private ?ColumnVisibilityGroupBuilderInterface $columnVisibilityGroupBuilder = null; + private ?ColumnVisibilityGroupFactoryInterface $columnVisibilityGroupFactory = null; private ?RequestHandlerInterface $requestHandler = null; private bool $sortingClearable = false; @@ -193,22 +193,22 @@ public function setColumnFactory(ColumnFactoryInterface $columnFactory): static return $this; } - public function getColumnVisibilityGroupBuilder(): ColumnVisibilityGroupBuilderInterface + public function getColumnVisibilityGroupFactory(): ColumnVisibilityGroupFactoryInterface { - if (!isset($this->columnVisibilityGroupBuilder)) { - throw new BadMethodCallException(sprintf('The column factory is not set, use the "%s::setColumnVisibilityGroupBuilder()" method to set the column factory.', $this::class)); + if (!isset($this->columnVisibilityGroupFactory)) { + throw new BadMethodCallException(sprintf('The column visibility group factory is not set, use the "%s::setColumnVisibilityGroupFactory()" method to set the factory.', $this::class)); } - return $this->columnVisibilityGroupBuilder; + return $this->columnVisibilityGroupFactory; } - public function setColumnVisibilityGroupBuilder(ColumnVisibilityGroupBuilderInterface $columnVisibilityGroupBuilder): static + public function setColumnVisibilityGroupFactory(ColumnVisibilityGroupFactoryInterface $columnVisibilityGroupFactory): static { if ($this->locked) { throw $this->createBuilderLockedException(); } - $this->columnVisibilityGroupBuilder = $columnVisibilityGroupBuilder; + $this->columnVisibilityGroupFactory = $columnVisibilityGroupFactory; return $this; } diff --git a/src/DataTableConfigBuilderInterface.php b/src/DataTableConfigBuilderInterface.php index 66207850..88b5bb0b 100755 --- a/src/DataTableConfigBuilderInterface.php +++ b/src/DataTableConfigBuilderInterface.php @@ -6,7 +6,7 @@ use Kreyu\Bundle\DataTableBundle\Action\ActionFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface; -use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactoryInterface; use Kreyu\Bundle\DataTableBundle\Exporter\ExportData; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface; use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface; @@ -52,7 +52,7 @@ public function setActionFactory(ActionFactoryInterface $actionFactory): static; public function setExporterFactory(ExporterFactoryInterface $exporterFactory): static; - public function setColumnVisibilityGroupBuilder(ColumnVisibilityGroupBuilderInterface $columnVisibilityGroupBuilder): static; + public function setColumnVisibilityGroupFactory(ColumnVisibilityGroupFactoryInterface $columnVisibilityGroupFactory): static; public function setExportingEnabled(bool $exportingEnabled): static; diff --git a/src/DataTableConfigInterface.php b/src/DataTableConfigInterface.php index 374ed1da..01fb33f8 100755 --- a/src/DataTableConfigInterface.php +++ b/src/DataTableConfigInterface.php @@ -6,6 +6,7 @@ use Kreyu\Bundle\DataTableBundle\Action\ActionFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactoryInterface; use Kreyu\Bundle\DataTableBundle\Exporter\ExportData; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterFactoryInterface; use Kreyu\Bundle\DataTableBundle\Filter\FilterFactoryInterface; @@ -52,6 +53,8 @@ public function getActionFactory(): ActionFactoryInterface; public function getExporterFactory(): ExporterFactoryInterface; + public function getColumnVisibilityGroupFactory(): ColumnVisibilityGroupFactoryInterface; + public function isExportingEnabled(): bool; public function getExportFormFactory(): ?FormFactoryInterface; diff --git a/src/DataTableInterface.php b/src/DataTableInterface.php index 9e2cc0c4..3373c490 100755 --- a/src/DataTableInterface.php +++ b/src/DataTableInterface.php @@ -223,8 +223,12 @@ public function isRequestFromTurboFrame(): bool; public function setRequestedColumnVisibilityGroup(?string $requestedColumnVisibilityGroup): self; + public function getRequestedColumnVisibilityGroup(): ?string; + /** * @return array */ public function getColumnVisibilityGroups(): array; + + public function addColumnVisibilityGroup(ColumnVisibilityGroupInterface $columnVisibilityGroup): static; } diff --git a/src/DataTableView.php b/src/DataTableView.php index b98c0542..ccd0bd0e 100755 --- a/src/DataTableView.php +++ b/src/DataTableView.php @@ -5,7 +5,7 @@ namespace Kreyu\Bundle\DataTableBundle; use Kreyu\Bundle\DataTableBundle\Action\ActionView; -use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupView; use Kreyu\Bundle\DataTableBundle\Filter\FilterView; use Kreyu\Bundle\DataTableBundle\Pagination\PaginationView; @@ -35,7 +35,7 @@ class DataTableView public array $actions = []; /** - * @var array + * @var array */ public array $columnVisibilityGroups = []; } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 571cda97..eaa03c41 100755 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -48,8 +48,8 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarNode('action_factory') ->defaultValue('kreyu_data_table.action.factory') ->end() - ->scalarNode('column_visibility_group_builder') - ->defaultValue('kreyu_data_table.column_visibility_group.builder') + ->scalarNode('column_visibility_group_factory') + ->defaultValue('kreyu_data_table.column_visibility_group.factory') ->end() ->scalarNode('request_handler') ->defaultValue('kreyu_data_table.request_handler.http_foundation') diff --git a/src/DependencyInjection/KreyuDataTableExtension.php b/src/DependencyInjection/KreyuDataTableExtension.php index 06b756d4..67281ddd 100755 --- a/src/DependencyInjection/KreyuDataTableExtension.php +++ b/src/DependencyInjection/KreyuDataTableExtension.php @@ -116,7 +116,7 @@ private function resolveConfiguration(array $configs, ContainerBuilder $containe 'action_factory', 'filter_factory', 'exporter_factory', - 'column_visibility_group_builder', + 'column_visibility_group_factory', 'request_handler', ]; diff --git a/src/Request/HttpFoundationRequestHandler.php b/src/Request/HttpFoundationRequestHandler.php index fdf3ac6d..8cac73d8 100755 --- a/src/Request/HttpFoundationRequestHandler.php +++ b/src/Request/HttpFoundationRequestHandler.php @@ -157,27 +157,29 @@ private function turbo(DataTableInterface $dataTable, Request $request): void private function columnVisibilityGroup(DataTableInterface $dataTable, Request $request): void { + $groups = $dataTable->getColumnVisibilityGroups(); + + if (empty($groups)) { + return; + } + $parameterName = $dataTable->getConfig()->getColumnVisibilityGroupParameterName(); + $requested = $this->extractQueryParameter($request, "[$parameterName]"); - $requestColumnVisibilityGroup = $this->extractQueryParameter($request, "[$parameterName]"); + if (null !== $requested && isset($groups[$requested])) { + $dataTable->setRequestedColumnVisibilityGroup($requested); - $mustBeDefault = false; - if (null === $requestColumnVisibilityGroup) { - $mustBeDefault = true; + return; } - // This also checks if the requested column visibility group exists - foreach ($dataTable->getColumnVisibilityGroups() as $columnVisibilityGroup) { - if ( - ($mustBeDefault && $columnVisibilityGroup->isDefault()) - || $columnVisibilityGroup->getName() === $requestColumnVisibilityGroup - ) { - $columnVisibilityGroup->setIsSelected(true); - if ($mustBeDefault) { - $requestColumnVisibilityGroup = $columnVisibilityGroup->getName(); - } - $dataTable->setRequestedColumnVisibilityGroup($requestColumnVisibilityGroup); + foreach ($groups as $group) { + if ($group->isDefault()) { + $dataTable->setRequestedColumnVisibilityGroup($group->getName()); + + return; } } + + $dataTable->setRequestedColumnVisibilityGroup(array_key_first($groups)); } } diff --git a/src/Resources/config/columns.php b/src/Resources/config/columns.php index 6711b833..b558bc50 100755 --- a/src/Resources/config/columns.php +++ b/src/Resources/config/columns.php @@ -26,8 +26,8 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TemplateColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; -use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilder; -use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactory; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactoryInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -59,9 +59,9 @@ ; $services - ->set('kreyu_data_table.column_visibility_group.builder', ColumnVisibilityGroupBuilder::class) + ->set('kreyu_data_table.column_visibility_group.factory', ColumnVisibilityGroupFactory::class) ->args([service('translator')]) - ->alias(ColumnVisibilityGroupBuilderInterface::class, 'kreyu_data_table.column_visibility_group.builder') + ->alias(ColumnVisibilityGroupFactoryInterface::class, 'kreyu_data_table.column_visibility_group.factory') ; $services diff --git a/src/Type/DataTableType.php b/src/Type/DataTableType.php index 62cb4b1f..cd6d773a 100755 --- a/src/Type/DataTableType.php +++ b/src/Type/DataTableType.php @@ -9,7 +9,9 @@ use Kreyu\Bundle\DataTableBundle\Action\ActionView; use Kreyu\Bundle\DataTableBundle\Column\ColumnFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; -use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupBuilderInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactoryInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupInterface; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupView; use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Kreyu\Bundle\DataTableBundle\DataTableInterface; use Kreyu\Bundle\DataTableBundle\DataTableView; @@ -44,7 +46,7 @@ public function buildDataTable(DataTableBuilderInterface $builder, array $option $setters = [ 'themes' => $builder->setThemes(...), 'column_factory' => $builder->setColumnFactory(...), - 'column_visibility_group_builder' => $builder->setColumnVisibilityGroupBuilder(...), + 'column_visibility_group_factory' => $builder->setColumnVisibilityGroupFactory(...), 'filter_factory' => $builder->setFilterFactory(...), 'action_factory' => $builder->setActionFactory(...), 'exporter_factory' => $builder->setExporterFactory(...), @@ -118,7 +120,7 @@ public function buildView(DataTableView $view, DataTableInterface $dataTable, ar $view->pagination = $this->createPaginationView($view, $dataTable); $view->filters = $this->createFilterViews($view, $dataTable); $view->actions = $this->createActionViews($view, $dataTable); - $view->columnVisibilityGroups = $dataTable->getColumnVisibilityGroups(); + $view->columnVisibilityGroups = $this->createColumnVisibilityGroupViews($dataTable); $view->vars = array_replace($view->vars, [ 'header_row' => $view->headerRow, @@ -167,7 +169,7 @@ public function configureOptions(OptionsResolver $resolver): void 'filter_factory' => $this->defaults['filtration']['filter_factory'] ?? null, 'action_factory' => $this->defaults['action_factory'] ?? null, 'exporter_factory' => $this->defaults['exporting']['exporter_factory'] ?? null, - 'column_visibility_group_builder' => $this->defaults['column_visibility_group_builder'] ?? null, + 'column_visibility_group_factory' => $this->defaults['column_visibility_group_factory'] ?? null, 'request_handler' => $this->defaults['request_handler'] ?? null, 'sorting_enabled' => $this->defaults['sorting']['enabled'] ?? true, 'sorting_clearable' => $this->defaults['sorting']['clearable'] ?? true, @@ -200,7 +202,7 @@ public function configureOptions(OptionsResolver $resolver): void ->setAllowedTypes('filter_factory', ['null', FilterFactoryInterface::class]) ->setAllowedTypes('action_factory', ['null', ActionFactoryInterface::class]) ->setAllowedTypes('exporter_factory', ['null', ExporterFactoryInterface::class]) - ->setAllowedTypes('column_visibility_group_builder', ['null', ColumnVisibilityGroupBuilderInterface::class]) + ->setAllowedTypes('column_visibility_group_factory', ['null', ColumnVisibilityGroupFactoryInterface::class]) ->setAllowedTypes('request_handler', ['null', RequestHandlerInterface::class]) ->setAllowedTypes('sorting_enabled', 'bool') ->setAllowedTypes('sorting_clearable', 'bool') @@ -267,6 +269,32 @@ private function createBatchActionViews(DataTableView $view, DataTableInterface ); } + /** + * @return array + */ + private function createColumnVisibilityGroupViews(DataTableInterface $dataTable): array + { + $requested = $dataTable->getRequestedColumnVisibilityGroup(); + + $views = []; + + foreach ($dataTable->getColumnVisibilityGroups() as $group) { + $views[$group->getName()] = $this->createColumnVisibilityGroupView($group, $requested); + } + + return $views; + } + + private function createColumnVisibilityGroupView(ColumnVisibilityGroupInterface $group, ?string $requested): ColumnVisibilityGroupView + { + return new ColumnVisibilityGroupView( + name: $group->getName(), + label: $group->getLabel(), + isDefault: $group->isDefault(), + isSelected: $group->getName() === $requested, + ); + } + /** * @return array */ From a02935aa11de704fb5b5353965baff92ab7b7d8f Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 17 Apr 2026 09:56:01 +0200 Subject: [PATCH 09/13] Add tests --- src/Test/DataTableIntegrationTestCase.php | 17 +++++ tests/Unit/DataTableBuilderTest.php | 82 +++++++++++++++++++++++ tests/Unit/DataTableTest.php | 56 ++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/src/Test/DataTableIntegrationTestCase.php b/src/Test/DataTableIntegrationTestCase.php index c804a2aa..fcfd32df 100644 --- a/src/Test/DataTableIntegrationTestCase.php +++ b/src/Test/DataTableIntegrationTestCase.php @@ -12,6 +12,8 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactory; use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactory; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactoryInterface; use Kreyu\Bundle\DataTableBundle\DataTableFactory; use Kreyu\Bundle\DataTableBundle\DataTableFactoryInterface; use Kreyu\Bundle\DataTableBundle\DataTableRegistry; @@ -21,6 +23,7 @@ use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactory; use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactoryInterface; use PHPUnit\Framework\TestCase; +use Symfony\Contracts\Translation\TranslatorInterface; abstract class DataTableIntegrationTestCase extends TestCase { @@ -54,6 +57,7 @@ protected function getDataTableTypes(): array return [ new DataTableType([ 'column_factory' => $this->createColumnFactory(), + 'column_visibility_group_factory' => $this->createColumnVisibilityGroupFactory(), ]), ]; } @@ -106,4 +110,17 @@ protected function getResolvedColumnTypeFactory(): ResolvedColumnTypeFactoryInte { return new ResolvedColumnTypeFactory(); } + + protected function createColumnVisibilityGroupFactory(): ColumnVisibilityGroupFactoryInterface + { + return new ColumnVisibilityGroupFactory($this->createTranslator()); + } + + protected function createTranslator(): TranslatorInterface + { + $translator = $this->createStub(TranslatorInterface::class); + $translator->method('trans')->willReturnArgument(0); + + return $translator; + } } diff --git a/tests/Unit/DataTableBuilderTest.php b/tests/Unit/DataTableBuilderTest.php index d9837dfc..2732b74a 100644 --- a/tests/Unit/DataTableBuilderTest.php +++ b/tests/Unit/DataTableBuilderTest.php @@ -21,6 +21,7 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactory; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactory; use Kreyu\Bundle\DataTableBundle\DataTableBuilder; use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface; @@ -41,6 +42,7 @@ use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class DataTableBuilderTest extends TestCase { @@ -717,6 +719,78 @@ public function testGetDataTableResolvesActions() $this->assertEquals($expectedActions, $dataTable->getActions()); } + public function testAddColumnVisibilityGroupAsString() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + + $builder->addColumnVisibilityGroup('foo'); + + $this->assertTrue($builder->hasColumnVisibilityGroup('foo')); + } + + public function testAddColumnVisibilityGroupAsObject() + { + $factory = $this->createColumnVisibilityGroupFactory(); + $group = $factory->create('foo'); + + $builder = $this->createBuilder(); + $builder->addColumnVisibilityGroup($group); + + $this->assertTrue($builder->hasColumnVisibilityGroup('foo')); + } + + public function testRemoveColumnVisibilityGroup() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + + $builder->addColumnVisibilityGroup('foo'); + $builder->removeColumnVisibilityGroup('foo'); + + $this->assertFalse($builder->hasColumnVisibilityGroup('foo')); + } + + public function testCreateColumnVisibilityGroup() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + + $group = $builder->createColumnVisibilityGroup('foo', ['label' => 'Foo group']); + + $this->assertSame('foo', $group->getName()); + $this->assertSame('Foo group', $group->getLabel()); + } + + public function testGetDataTableResolvesColumnVisibilityGroups() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + $builder->addColumnVisibilityGroup('foo'); + $builder->addColumnVisibilityGroup('bar', ['is_default' => true]); + + $dataTable = $builder->getDataTable(); + + $groups = $dataTable->getColumnVisibilityGroups(); + + $this->assertCount(2, $groups); + $this->assertFalse($groups['foo']->isDefault()); + $this->assertTrue($groups['bar']->isDefault()); + } + + public function testGetDataTableThrowsWhenMultipleDefaultGroups() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + $builder->addColumnVisibilityGroup('foo', ['is_default' => true]); + $builder->addColumnVisibilityGroup('bar', ['is_default' => true]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only one column visibility group can be marked as default'); + + $builder->getDataTable(); + } + public function testGetDataTableResolvesExporters() { $builder = $this->createBuilder(); @@ -791,6 +865,14 @@ private function createActionFactory(): ActionFactory ); } + private function createColumnVisibilityGroupFactory(): ColumnVisibilityGroupFactory + { + $translator = $this->createStub(TranslatorInterface::class); + $translator->method('trans')->willReturnArgument(0); + + return new ColumnVisibilityGroupFactory($translator); + } + private function createExporterFactory(): ExporterFactory { return new ExporterFactory( diff --git a/tests/Unit/DataTableTest.php b/tests/Unit/DataTableTest.php index 22ef1fe4..1e39c3dd 100644 --- a/tests/Unit/DataTableTest.php +++ b/tests/Unit/DataTableTest.php @@ -144,6 +144,62 @@ public function testGetVisibleColumnsIgnoresDisabledPersonalization() $this->assertEquals(['fifth', 'third', 'first'], $columns); } + public function testGetVisibleColumnsWithoutRequestedGroupShowsAll() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('always', options: ['priority' => 3]) + ->addColumn('foo_only', options: ['priority' => 2, 'column_visibility_groups' => ['foo']]) + ->addColumn('bar_only', options: ['priority' => 1, 'column_visibility_groups' => ['bar']]) + ->getDataTable(); + + $columns = array_keys($dataTable->getVisibleColumns()); + + $this->assertEquals(['always', 'foo_only', 'bar_only'], $columns); + } + + public function testGetVisibleColumnsFiltersByRequestedGroup() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('always', options: ['priority' => 3]) + ->addColumn('foo_only', options: ['priority' => 2, 'column_visibility_groups' => ['foo']]) + ->addColumn('bar_only', options: ['priority' => 1, 'column_visibility_groups' => ['bar']]) + ->getDataTable(); + + $dataTable->setRequestedColumnVisibilityGroup('foo'); + + $columns = array_keys($dataTable->getVisibleColumns()); + + $this->assertEquals(['always', 'foo_only'], $columns); + } + + public function testGetVisibleColumnsSupportsMultipleGroupsPerColumn() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('shared', options: ['column_visibility_groups' => ['foo', 'bar']]) + ->addColumn('foo_only', options: ['column_visibility_groups' => ['foo']]) + ->getDataTable(); + + $dataTable->setRequestedColumnVisibilityGroup('bar'); + + $this->assertEquals(['shared'], array_keys($dataTable->getVisibleColumns())); + } + + public function testPersonalizationHiddenWinsOverVisibilityGroup() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => true]) + ->addColumn('foo_only', options: ['column_visibility_groups' => ['foo']]) + ->getDataTable(); + + $dataTable->setRequestedColumnVisibilityGroup('foo'); + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'foo_only' => ['visible' => false], + ], + ])); + + $this->assertEmpty($dataTable->getVisibleColumns()); + } + public function testGetHiddenColumns() { $dataTable = $this->createDataTableBuilder() From 8dbacc078b4f899f63c97cd21626bf1c5ddc2194 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 17 Apr 2026 09:56:01 +0200 Subject: [PATCH 10/13] Add tests --- src/Test/DataTableIntegrationTestCase.php | 17 ++++ .../ColumnVisibilityGroupFactoryTest.php | 76 ++++++++++++++++ tests/Unit/DataTableBuilderTest.php | 82 +++++++++++++++++ tests/Unit/DataTableTest.php | 56 ++++++++++++ .../HttpFoundationRequestHandlerTest.php | 91 +++++++++++++++++++ 5 files changed, 322 insertions(+) create mode 100644 tests/Unit/ColumnVisibilityGroup/ColumnVisibilityGroupFactoryTest.php create mode 100644 tests/Unit/Request/HttpFoundationRequestHandlerTest.php diff --git a/src/Test/DataTableIntegrationTestCase.php b/src/Test/DataTableIntegrationTestCase.php index c804a2aa..fcfd32df 100644 --- a/src/Test/DataTableIntegrationTestCase.php +++ b/src/Test/DataTableIntegrationTestCase.php @@ -12,6 +12,8 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactory; use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactoryInterface; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactory; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactoryInterface; use Kreyu\Bundle\DataTableBundle\DataTableFactory; use Kreyu\Bundle\DataTableBundle\DataTableFactoryInterface; use Kreyu\Bundle\DataTableBundle\DataTableRegistry; @@ -21,6 +23,7 @@ use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactory; use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeFactoryInterface; use PHPUnit\Framework\TestCase; +use Symfony\Contracts\Translation\TranslatorInterface; abstract class DataTableIntegrationTestCase extends TestCase { @@ -54,6 +57,7 @@ protected function getDataTableTypes(): array return [ new DataTableType([ 'column_factory' => $this->createColumnFactory(), + 'column_visibility_group_factory' => $this->createColumnVisibilityGroupFactory(), ]), ]; } @@ -106,4 +110,17 @@ protected function getResolvedColumnTypeFactory(): ResolvedColumnTypeFactoryInte { return new ResolvedColumnTypeFactory(); } + + protected function createColumnVisibilityGroupFactory(): ColumnVisibilityGroupFactoryInterface + { + return new ColumnVisibilityGroupFactory($this->createTranslator()); + } + + protected function createTranslator(): TranslatorInterface + { + $translator = $this->createStub(TranslatorInterface::class); + $translator->method('trans')->willReturnArgument(0); + + return $translator; + } } diff --git a/tests/Unit/ColumnVisibilityGroup/ColumnVisibilityGroupFactoryTest.php b/tests/Unit/ColumnVisibilityGroup/ColumnVisibilityGroupFactoryTest.php new file mode 100644 index 00000000..76a93064 --- /dev/null +++ b/tests/Unit/ColumnVisibilityGroup/ColumnVisibilityGroupFactoryTest.php @@ -0,0 +1,76 @@ +createFactory()->create('address'); + + $this->assertSame('address', $group->getName()); + $this->assertSame('address', $group->getLabel()); + $this->assertFalse($group->isDefault()); + } + + public function testCreateWithLabel() + { + $group = $this->createFactory()->create('address', ['label' => 'Address details']); + + $this->assertSame('Address details', $group->getLabel()); + } + + public function testCreateTranslatesLabel() + { + $translator = $this->createStub(TranslatorInterface::class); + $translator->method('trans')->willReturnCallback(static fn (string $id) => strtoupper($id)); + + $group = (new ColumnVisibilityGroupFactory($translator))->create('address', ['label' => 'foo']); + + $this->assertSame('FOO', $group->getLabel()); + } + + public function testCreateWithIsDefault() + { + $group = $this->createFactory()->create('address', ['is_default' => true]); + + $this->assertTrue($group->isDefault()); + } + + public function testCreateRejectsInvalidOption() + { + $this->expectException(UndefinedOptionsException::class); + + $this->createFactory()->create('address', ['unknown' => 'foo']); + } + + public function testCreateRejectsInvalidLabelType() + { + $this->expectException(InvalidOptionsException::class); + + $this->createFactory()->create('address', ['label' => 42]); + } + + public function testCreateRejectsInvalidIsDefaultType() + { + $this->expectException(InvalidOptionsException::class); + + $this->createFactory()->create('address', ['is_default' => 'yes']); + } + + private function createFactory(): ColumnVisibilityGroupFactory + { + $translator = $this->createStub(TranslatorInterface::class); + $translator->method('trans')->willReturnArgument(0); + + return new ColumnVisibilityGroupFactory($translator); + } +} diff --git a/tests/Unit/DataTableBuilderTest.php b/tests/Unit/DataTableBuilderTest.php index d9837dfc..2732b74a 100644 --- a/tests/Unit/DataTableBuilderTest.php +++ b/tests/Unit/DataTableBuilderTest.php @@ -21,6 +21,7 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\ResolvedColumnTypeFactory; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; +use Kreyu\Bundle\DataTableBundle\ColumnVisibilityGroup\ColumnVisibilityGroupFactory; use Kreyu\Bundle\DataTableBundle\DataTableBuilder; use Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException; use Kreyu\Bundle\DataTableBundle\Exporter\ExporterBuilderInterface; @@ -41,6 +42,7 @@ use Kreyu\Bundle\DataTableBundle\Type\ResolvedDataTableTypeInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class DataTableBuilderTest extends TestCase { @@ -717,6 +719,78 @@ public function testGetDataTableResolvesActions() $this->assertEquals($expectedActions, $dataTable->getActions()); } + public function testAddColumnVisibilityGroupAsString() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + + $builder->addColumnVisibilityGroup('foo'); + + $this->assertTrue($builder->hasColumnVisibilityGroup('foo')); + } + + public function testAddColumnVisibilityGroupAsObject() + { + $factory = $this->createColumnVisibilityGroupFactory(); + $group = $factory->create('foo'); + + $builder = $this->createBuilder(); + $builder->addColumnVisibilityGroup($group); + + $this->assertTrue($builder->hasColumnVisibilityGroup('foo')); + } + + public function testRemoveColumnVisibilityGroup() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + + $builder->addColumnVisibilityGroup('foo'); + $builder->removeColumnVisibilityGroup('foo'); + + $this->assertFalse($builder->hasColumnVisibilityGroup('foo')); + } + + public function testCreateColumnVisibilityGroup() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + + $group = $builder->createColumnVisibilityGroup('foo', ['label' => 'Foo group']); + + $this->assertSame('foo', $group->getName()); + $this->assertSame('Foo group', $group->getLabel()); + } + + public function testGetDataTableResolvesColumnVisibilityGroups() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + $builder->addColumnVisibilityGroup('foo'); + $builder->addColumnVisibilityGroup('bar', ['is_default' => true]); + + $dataTable = $builder->getDataTable(); + + $groups = $dataTable->getColumnVisibilityGroups(); + + $this->assertCount(2, $groups); + $this->assertFalse($groups['foo']->isDefault()); + $this->assertTrue($groups['bar']->isDefault()); + } + + public function testGetDataTableThrowsWhenMultipleDefaultGroups() + { + $builder = $this->createBuilder(); + $builder->setColumnVisibilityGroupFactory($this->createColumnVisibilityGroupFactory()); + $builder->addColumnVisibilityGroup('foo', ['is_default' => true]); + $builder->addColumnVisibilityGroup('bar', ['is_default' => true]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only one column visibility group can be marked as default'); + + $builder->getDataTable(); + } + public function testGetDataTableResolvesExporters() { $builder = $this->createBuilder(); @@ -791,6 +865,14 @@ private function createActionFactory(): ActionFactory ); } + private function createColumnVisibilityGroupFactory(): ColumnVisibilityGroupFactory + { + $translator = $this->createStub(TranslatorInterface::class); + $translator->method('trans')->willReturnArgument(0); + + return new ColumnVisibilityGroupFactory($translator); + } + private function createExporterFactory(): ExporterFactory { return new ExporterFactory( diff --git a/tests/Unit/DataTableTest.php b/tests/Unit/DataTableTest.php index 22ef1fe4..1e39c3dd 100644 --- a/tests/Unit/DataTableTest.php +++ b/tests/Unit/DataTableTest.php @@ -144,6 +144,62 @@ public function testGetVisibleColumnsIgnoresDisabledPersonalization() $this->assertEquals(['fifth', 'third', 'first'], $columns); } + public function testGetVisibleColumnsWithoutRequestedGroupShowsAll() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('always', options: ['priority' => 3]) + ->addColumn('foo_only', options: ['priority' => 2, 'column_visibility_groups' => ['foo']]) + ->addColumn('bar_only', options: ['priority' => 1, 'column_visibility_groups' => ['bar']]) + ->getDataTable(); + + $columns = array_keys($dataTable->getVisibleColumns()); + + $this->assertEquals(['always', 'foo_only', 'bar_only'], $columns); + } + + public function testGetVisibleColumnsFiltersByRequestedGroup() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('always', options: ['priority' => 3]) + ->addColumn('foo_only', options: ['priority' => 2, 'column_visibility_groups' => ['foo']]) + ->addColumn('bar_only', options: ['priority' => 1, 'column_visibility_groups' => ['bar']]) + ->getDataTable(); + + $dataTable->setRequestedColumnVisibilityGroup('foo'); + + $columns = array_keys($dataTable->getVisibleColumns()); + + $this->assertEquals(['always', 'foo_only'], $columns); + } + + public function testGetVisibleColumnsSupportsMultipleGroupsPerColumn() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('shared', options: ['column_visibility_groups' => ['foo', 'bar']]) + ->addColumn('foo_only', options: ['column_visibility_groups' => ['foo']]) + ->getDataTable(); + + $dataTable->setRequestedColumnVisibilityGroup('bar'); + + $this->assertEquals(['shared'], array_keys($dataTable->getVisibleColumns())); + } + + public function testPersonalizationHiddenWinsOverVisibilityGroup() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => true]) + ->addColumn('foo_only', options: ['column_visibility_groups' => ['foo']]) + ->getDataTable(); + + $dataTable->setRequestedColumnVisibilityGroup('foo'); + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'foo_only' => ['visible' => false], + ], + ])); + + $this->assertEmpty($dataTable->getVisibleColumns()); + } + public function testGetHiddenColumns() { $dataTable = $this->createDataTableBuilder() diff --git a/tests/Unit/Request/HttpFoundationRequestHandlerTest.php b/tests/Unit/Request/HttpFoundationRequestHandlerTest.php new file mode 100644 index 00000000..6eb02249 --- /dev/null +++ b/tests/Unit/Request/HttpFoundationRequestHandlerTest.php @@ -0,0 +1,91 @@ +createDataTable(['foo', 'bar']); + + (new HttpFoundationRequestHandler())->handle( + $dataTable, + new Request([$this->parameterName($dataTable) => 'bar']), + ); + + $this->assertSame('bar', $dataTable->getRequestedColumnVisibilityGroup()); + } + + public function testColumnVisibilityGroupFallsBackToDefaultWhenParamMissing() + { + $dataTable = $this->createDataTable(['foo', 'bar'], defaultName: 'bar'); + + (new HttpFoundationRequestHandler())->handle($dataTable, new Request()); + + $this->assertSame('bar', $dataTable->getRequestedColumnVisibilityGroup()); + } + + public function testColumnVisibilityGroupFallsBackToFirstWhenNoDefault() + { + $dataTable = $this->createDataTable(['foo', 'bar']); + + (new HttpFoundationRequestHandler())->handle($dataTable, new Request()); + + $this->assertSame('foo', $dataTable->getRequestedColumnVisibilityGroup()); + } + + public function testColumnVisibilityGroupIgnoresUnknownRequestedAndUsesDefault() + { + $dataTable = $this->createDataTable(['foo', 'bar'], defaultName: 'bar'); + + (new HttpFoundationRequestHandler())->handle( + $dataTable, + new Request([$this->parameterName($dataTable) => 'unknown']), + ); + + $this->assertSame('bar', $dataTable->getRequestedColumnVisibilityGroup()); + } + + public function testColumnVisibilityGroupIsNoOpWhenNoGroupsConfigured() + { + $dataTable = $this->createDataTable([]); + + (new HttpFoundationRequestHandler())->handle( + $dataTable, + new Request([$this->parameterName($dataTable) => 'foo']), + ); + + $this->assertNull($dataTable->getRequestedColumnVisibilityGroup()); + } + + private function parameterName(DataTableInterface $dataTable): string + { + return $dataTable->getConfig()->getColumnVisibilityGroupParameterName(); + } + + /** + * @param list $groupNames + */ + private function createDataTable(array $groupNames, ?string $defaultName = null): DataTableInterface + { + $builder = $this->dataTableFactory->createBuilder(DataTableType::class, []); + $builder->addColumn('name', TextColumnType::class); + + foreach ($groupNames as $name) { + $builder->addColumnVisibilityGroup($name, [ + 'is_default' => $name === $defaultName, + ]); + } + + return $builder->getDataTable(); + } +} From ff8eb46d95444d5f03420d52c3b12672f76e7d83 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 17 Apr 2026 10:23:12 +0200 Subject: [PATCH 11/13] Add tests and implementation for preserving query parameters in column visibility group selector --- src/Resources/views/themes/base.html.twig | 16 +++ .../views/themes/bootstrap_5.html.twig | 2 +- ...ColumnVisibilityGroupSelectorBlockTest.php | 104 ++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Twig/ColumnVisibilityGroupSelectorBlockTest.php diff --git a/src/Resources/views/themes/base.html.twig b/src/Resources/views/themes/base.html.twig index 90d76fc8..40043374 100755 --- a/src/Resources/views/themes/base.html.twig +++ b/src/Resources/views/themes/base.html.twig @@ -69,6 +69,10 @@ {% set form_attr = { 'data-turbo-frame': '_self', 'data-turbo-action': 'advance' }|merge(form_attr ?? {}) %}
+ {% for name, value in data_table.vars.url_query_parameters ?? {} %} + {{ block('column_visibility_group_selector_hidden_input', theme) }} + {% endfor %} + {% set select_attr = { name: data_table.vars.column_visibility_group_parameter_name, autocomplete: 'off', @@ -84,6 +88,18 @@ {% endif %} {% endblock %} +{% block column_visibility_group_selector_hidden_input %} + {% if value is iterable %} + {% for sub_key, sub_value in value %} + {% with { name: name ~ '[' ~ sub_key ~ ']', value: sub_value } %} + {{ block('column_visibility_group_selector_hidden_input', theme) }} + {% endwith %} + {% endfor %} + {% else %} + + {% endif %} +{% endblock %} + {% block table %}
{{ block('table_head', theme) }} diff --git a/src/Resources/views/themes/bootstrap_5.html.twig b/src/Resources/views/themes/bootstrap_5.html.twig index b041c6fd..e141b48b 100755 --- a/src/Resources/views/themes/bootstrap_5.html.twig +++ b/src/Resources/views/themes/bootstrap_5.html.twig @@ -24,7 +24,7 @@ {% set display_filter_action = filtration_enabled and filters|length > 0 and filtration_form.children|length > 0 %} {% set display_export_action = exporting_enabled and exporters|length > 0 %} - {% if title or actions is not empty or has_active_filters or display_filter_action or display_export_action or personalization_enabled %} + {% if title or actions is not empty or has_active_filters or display_filter_action or display_export_action or personalization_enabled or data_table.columnVisibilityGroups is not empty %}
{% if translation_domain is not same as false %} diff --git a/tests/Unit/Twig/ColumnVisibilityGroupSelectorBlockTest.php b/tests/Unit/Twig/ColumnVisibilityGroupSelectorBlockTest.php new file mode 100644 index 00000000..828c7c46 --- /dev/null +++ b/tests/Unit/Twig/ColumnVisibilityGroupSelectorBlockTest.php @@ -0,0 +1,104 @@ +renderSelector([ + 'page_foo' => 2, + 'limit_foo' => 25, + ]); + + $this->assertStringContainsString('', $html); + $this->assertStringContainsString('', $html); + } + + public function testFormIncludesHiddenInputsForNestedQueryParameters(): void + { + $html = $this->renderSelector([ + 'sort_foo' => ['id' => 'asc', 'name' => 'desc'], + ]); + + $this->assertStringContainsString('', $html); + $this->assertStringContainsString('', $html); + } + + public function testFormIncludesHiddenInputsForDeeplyNestedQueryParameters(): void + { + $html = $this->renderSelector([ + 'filter_foo' => [ + 'status' => ['value' => 'active', 'operator' => 'eq'], + ], + ]); + + $this->assertStringContainsString('', $html); + $this->assertStringContainsString('', $html); + } + + public function testFormHasNoHiddenInputsWhenNoQueryParameters(): void + { + $html = $this->renderSelector([]); + + $this->assertStringNotContainsString('renderSelector([], columnVisibilityGroups: []); + + $this->assertSame('', trim($html)); + } + + private function renderSelector(array $urlQueryParameters, ?array $columnVisibilityGroups = null): string + { + $loader = new FilesystemLoader(__DIR__.'/../../../src/Resources/views/themes'); + $twig = new Environment($loader, ['strict_variables' => false]); + $twig->addExtension(new TranslationExtension(new Translator('en'))); + $twig->addExtension(new FormExtension()); + $twig->addExtension(new IntlExtension()); + $twig->addExtension(new DataTableExtension( + $this->createStub(ColumnSortUrlGeneratorInterface::class), + $this->createStub(FilterClearUrlGeneratorInterface::class), + $this->createStub(PaginationUrlGeneratorInterface::class), + )); + + $template = $twig->load('base.html.twig'); + + $view = new DataTableView(); + $view->vars = [ + 'url_query_parameters' => $urlQueryParameters, + 'column_visibility_group_parameter_name' => 'column_visibility_group_foo', + ]; + $view->columnVisibilityGroups = $columnVisibilityGroups ?? [ + 'default' => new ColumnVisibilityGroupView('default', 'Default', true, true), + ]; + + return $template->renderBlock('column_visibility_group_selector', [ + 'data_table' => $view, + 'theme' => $template, + ]); + } +} From 0bd4f560e9b0d51cbed90520df29dbfeb1a021b6 Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 17 Apr 2026 11:31:20 +0200 Subject: [PATCH 12/13] Validate column visibility groups and document usage --- docs/.vitepress/config.mts | 1 + .../docs/features/column-visibility-group.md | 228 +++++++++++++++--- src/DataTableBuilder.php | 10 + tests/Unit/DataTableTest.php | 19 ++ 4 files changed, 225 insertions(+), 33 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index a8f5a713..78da69ec 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -50,6 +50,7 @@ export default defineConfig({ { text: 'Exporting', link: '/docs/features/exporting' }, { text: 'Pagination', link: '/docs/features/pagination' }, { text: 'Personalization', link: '/docs/features/personalization' }, + { text: 'Column visibility groups', link: '/docs/features/column-visibility-group' }, { text: 'Persistence', link: '/docs/features/persistence' }, { text: 'Theming', link: '/docs/features/theming' }, { text: 'Asynchronicity', link: '/docs/features/asynchronicity' }, diff --git a/docs/src/docs/features/column-visibility-group.md b/docs/src/docs/features/column-visibility-group.md index ef0b2f79..ba7b7d7b 100644 --- a/docs/src/docs/features/column-visibility-group.md +++ b/docs/src/docs/features/column-visibility-group.md @@ -1,12 +1,22 @@ # Column Visibility Groups -Column Visibility Groups allow you to organize table columns into different "views." This is useful when you have a lot of information to display in a single row and want to separate it into multiple, easily switchable groups. Users can select which group of columns to display using a dropdown in the table UI. +Column visibility groups let you define several **predefined views** over the same data table. +Each group is a curated subset of columns, and the user switches between them via a dropdown rendered above the table. +Typical use cases: showing "general" vs "details" views on a wide table, or giving different roles their own column sets. + +::: tip Visibility groups vs personalization +Visibility groups are for **curated, designer-controlled** column sets defined in code. The +[personalization feature](personalization.md) is for **user-controlled** column selection saved per +user. The two features compose (see [Interactions](#interactions-with-other-features)) but serve +different intents. +::: [[toc]] ## Basic Usage -By default, a data table has a single visibility group. You can define additional groups and assign columns to them. +Define groups with `$builder->addColumnVisibilityGroup()`, then opt columns into one or more groups +with the `column_visibility_groups` column option. ```php use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; @@ -14,32 +24,27 @@ use Kreyu\Bundle\DataTableBundle\Type\AbstractDataTableType; use Kreyu\Bundle\DataTableBundle\Column\Type\NumberColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\TextColumnType; -class ExampleDataTableType extends AbstractDataTableType +class CustomerDataTableType extends AbstractDataTableType { public function buildDataTable(DataTableBuilderInterface $builder, array $options): void { - // Define visibility groups - $builder->addColumnVisibilityGroup('default'); - $builder->addColumnVisibilityGroup('address', [ - // By default, the group label is the group name, but you can override it: - 'label' => 'Address related content', - // By default, the first defined group is the default one, but you can override it: - 'is_default' => true, - ]); - - // Assign groups to columns $builder - ->addColumn('id', NumberColumnType::class, [ - 'sort' => true, - // Will always be displayed as it does not have any group assigned - ]) - ->addColumn('name', TextColumnType::class, [ - 'label' => 'Full name', - 'sort' => true, + ->addColumnVisibilityGroup('general', ['is_default' => true]) + ->addColumnVisibilityGroup('address', ['label' => 'Address details']) + ; + + $builder + // No "column_visibility_groups" option: always visible, in every group. + ->addColumn('id', NumberColumnType::class) + ->addColumn('name', TextColumnType::class) + + // Visible only when the "general" group is selected. + ->addColumn('email', TextColumnType::class, [ + 'column_visibility_groups' => ['general'], ]) + + // Visible only when the "address" group is selected. ->addColumn('streetName', TextColumnType::class, [ - 'sort' => true, - // This column will only be visible when the "address" group is selected 'column_visibility_groups' => ['address'], ]) ; @@ -47,19 +52,176 @@ class ExampleDataTableType extends AbstractDataTableType } ``` -## How It Works +As soon as a data table has at least one visibility group, a `` with nothing to switch to. Define at least two +groups — or don't define any and let the default "all columns visible" behavior do the job. +::: + +## Defining groups + +Each call to `addColumnVisibilityGroup($name, $options)` adds one group. The `$name` must be unique +within the data table and is used as the value in the URL query parameter. + +The following options are accepted: + +| Option | Type | Default | Description | +|--------------|------------------|----------|------------------------------------------------------------------------------| +| `label` | `null \| string` | `null` | Display name used in the dropdown. Falls back to the group `$name` if null. | +| `is_default` | `bool` | `false` | Marks this group as the default selection when no URL parameter is present. | + +::: warning Only one default group +At most **one** group may be marked as `is_default: true`. Setting two or more throws an +`InvalidArgumentException` the first time the data table is built (via `getDataTable()` or during +rendering) with a message listing the offending groups. +::: + +::: tip Fallback when no default is set +If no group is marked as default, the **first group defined** via `addColumnVisibilityGroup()` is +used as the default. +::: + +## Assigning columns to groups + +Use the `column_visibility_groups` option on `addColumn()`: + +```php +$builder + // Belongs to no group — always visible. + ->addColumn('id', NumberColumnType::class) + + // Shorthand: a single group name as a string. + ->addColumn('email', TextColumnType::class, [ + 'column_visibility_groups' => 'general', + ]) + + // Multiple groups: the column appears in any of them. + ->addColumn('phone', TextColumnType::class, [ + 'column_visibility_groups' => ['general', 'contact'], + ]) +; +``` + +The option accepts `null`, a `string`, or an `array` of strings. `null` or `[]` means the column +does not belong to any group and is visible regardless of the selected group. + +::: warning Unknown group names throw +If a column references a group name that was never defined with `addColumnVisibilityGroup()`, +`getDataTable()` throws an `InvalidArgumentException` listing the offending column, the unknown +group, and the groups that are actually defined. This catches typos at build time rather than +producing a silently hidden column. +::: + +## Default group resolution + +When the request arrives, the selected group is resolved in this order: + +1. The value of the URL query parameter if it matches a defined group name +2. Otherwise the group marked `is_default: true` +3. Otherwise the first group defined via `addColumnVisibilityGroup()` + +An invalid group name in the query parameter is treated as if no parameter was provided +(it falls through to step 2, then step 3). + +## URL query parameter + +The selected group is persisted in the URL so it survives refreshes and can be shared. +The parameter is `column_visibility_group` optionally suffixed with an underscore and the data table +name: + +``` +?column_visibility_group_=address +``` + +The data table name comes from `$dataTable->getConfig()->getName()`. By default it is derived from +the data table type's class name, stripped of its `DataTableType` suffix and converted to snake +case — `CustomerDataTableType` becomes `customer`, so the parameter is `column_visibility_group_customer`. +If the data table has no name, the parameter falls back to plain `column_visibility_group`. + +You can always read the exact parameter name from +`$dataTable->getConfig()->getColumnVisibilityGroupParameterName()` when you need to build URLs yourself. + +## Translating group labels + +Group labels are translated at build time using the default Symfony translator. If `label` is `null`, +the group `$name` is passed to the translator instead — so a technical name like `address` ends up +rendered in the UI if no translation exists for it. + +```php +$builder->addColumnVisibilityGroup('address', [ + 'label' => 'customer.visibility.address', +]); +``` + +::: warning Translation domain +The label is translated using the **default catalog** (typically `messages`), not the +`translation_domain` configured on the data table. Make sure your translation keys live in that +catalog, or pass a pre-translated string directly. +::: + +## Interactions with other features -- **Defining Groups:** Use `$builder->addColumnVisibilityGroup($name, $options)` to define one or more groups. The `label` option is used as the display name in the UI. -- **Assigning Columns:** Use the `column_visibility_groups` option in `addColumn()` to assign a column to one or more groups. If omitted or set to `null`/`[]`, the column will always be visible. -- **Switching Views:** A select dropdown appears in the table, allowing users to switch between the different column visibility groups. +### Personalization -## Notes +Personalization and visibility groups compose asymmetrically: + +- If personalization **hides** a column, it stays hidden regardless of the selected group. + Manual personalization wins. +- If personalization **shows** a column that the selected group excludes, the group still hides it. + The group wins. + +In other words, a column is visible only if both personalization and the selected group agree it +should be. + +### Sorting + +A column that is hidden — whether by personalization or because it does not belong to the selected +group — is removed from the header row, so its sort link is not rendered. Existing sort state on +a hidden column has no visible effect until the column becomes visible again. + +### Filtering + +::: warning Filters on hidden columns stay active +Switching to a group that does not contain a previously filtered column does **not** clear the +filter. The underlying query is still filtered, but the user has no visual indication of which +column is filtering the results. If this matters for your UX, clear filters explicitly when +switching groups. +::: + +### Pagination and sorting state + +When the user changes the selected group, the current `page`, `limit`, `sort`, and filter values +are re-submitted as hidden form inputs. The user stays on the same page with the same sort and +filters applied — only the visible columns change. + +## Customizing the UI + +The dropdown is rendered by the `column_visibility_group_selector` block defined in +`@KreyuDataTable/themes/base.html.twig`. Override it in your own theme to change the markup or +styling: + +```twig +{# templates/data_table/my_theme.html.twig #} +{% extends '@KreyuDataTable/themes/bootstrap_5.html.twig' %} + +{% block column_visibility_group_selector %} + {% if data_table.columnVisibilityGroups is not empty %} +
+ {{ parent() }} +
+ {% endif %} +{% endblock %} +``` -- You can define as many visibility groups as needed. -- A column can belong to multiple groups by specifying multiple group names in the `column_visibility_groups` array. -- If `column_visibility_groups` is `null` or an empty array, the column is visible in every group (it does not belong to any group and is therefore never filtered out). -- Creating a default group is optional but recommended for better user experience : it ensures that the user can go back to the base view. +The block receives: -## UI +- `data_table.columnVisibilityGroups` — an array of `ColumnVisibilityGroupView` with `name`, + `label`, `isDefault`, and `isSelected` public properties +- `data_table.vars.column_visibility_group_parameter_name` — the query parameter name +- `data_table.vars.url_query_parameters` — the current URL state (shared with pagination, sorting, + and filtering); re-emitted as hidden inputs to preserve page/sort/filter on group change -When multiple visibility groups are present, a select dropdown is rendered above the table, allowing users to choose which group of columns to display. +A companion block, `column_visibility_group_selector_hidden_input`, recursively renders the +hidden inputs for nested query parameters (filters in particular). diff --git a/src/DataTableBuilder.php b/src/DataTableBuilder.php index 43dcc5ed..dda36574 100755 --- a/src/DataTableBuilder.php +++ b/src/DataTableBuilder.php @@ -967,6 +967,16 @@ private function resolveColumnVisibilityGroups(): void if (count($defaults) > 1) { throw new InvalidArgumentException(sprintf('Only one column visibility group can be marked as default, but %d were found: "%s".', count($defaults), implode('", "', array_keys($defaults)))); } + + foreach ($this->columns as $column) { + $referenced = (array) $column->getColumnConfig()->getOption('column_visibility_groups'); + + foreach ($referenced as $groupName) { + if (!isset($this->columnVisibilityGroups[$groupName])) { + throw new InvalidArgumentException(sprintf('Column "%s" references the column visibility group "%s", but no such group has been defined on the data table. Available groups: %s.', $column->getName(), $groupName, $this->columnVisibilityGroups ? '"'.implode('", "', array_keys($this->columnVisibilityGroups)).'"' : 'none')); + } + } + } } private function resolveColumnVisibilityGroup(string $name): ColumnVisibilityGroupInterface diff --git a/tests/Unit/DataTableTest.php b/tests/Unit/DataTableTest.php index 1e39c3dd..3e82b534 100644 --- a/tests/Unit/DataTableTest.php +++ b/tests/Unit/DataTableTest.php @@ -147,6 +147,8 @@ public function testGetVisibleColumnsIgnoresDisabledPersonalization() public function testGetVisibleColumnsWithoutRequestedGroupShowsAll() { $dataTable = $this->createDataTableBuilder() + ->addColumnVisibilityGroup('foo') + ->addColumnVisibilityGroup('bar') ->addColumn('always', options: ['priority' => 3]) ->addColumn('foo_only', options: ['priority' => 2, 'column_visibility_groups' => ['foo']]) ->addColumn('bar_only', options: ['priority' => 1, 'column_visibility_groups' => ['bar']]) @@ -160,6 +162,8 @@ public function testGetVisibleColumnsWithoutRequestedGroupShowsAll() public function testGetVisibleColumnsFiltersByRequestedGroup() { $dataTable = $this->createDataTableBuilder() + ->addColumnVisibilityGroup('foo') + ->addColumnVisibilityGroup('bar') ->addColumn('always', options: ['priority' => 3]) ->addColumn('foo_only', options: ['priority' => 2, 'column_visibility_groups' => ['foo']]) ->addColumn('bar_only', options: ['priority' => 1, 'column_visibility_groups' => ['bar']]) @@ -175,6 +179,8 @@ public function testGetVisibleColumnsFiltersByRequestedGroup() public function testGetVisibleColumnsSupportsMultipleGroupsPerColumn() { $dataTable = $this->createDataTableBuilder() + ->addColumnVisibilityGroup('foo') + ->addColumnVisibilityGroup('bar') ->addColumn('shared', options: ['column_visibility_groups' => ['foo', 'bar']]) ->addColumn('foo_only', options: ['column_visibility_groups' => ['foo']]) ->getDataTable(); @@ -187,6 +193,7 @@ public function testGetVisibleColumnsSupportsMultipleGroupsPerColumn() public function testPersonalizationHiddenWinsOverVisibilityGroup() { $dataTable = $this->createDataTableBuilder(['personalization_enabled' => true]) + ->addColumnVisibilityGroup('foo') ->addColumn('foo_only', options: ['column_visibility_groups' => ['foo']]) ->getDataTable(); @@ -200,6 +207,18 @@ public function testPersonalizationHiddenWinsOverVisibilityGroup() $this->assertEmpty($dataTable->getVisibleColumns()); } + public function testGetDataTableThrowsWhenColumnReferencesUnknownGroup() + { + $builder = $this->createDataTableBuilder() + ->addColumnVisibilityGroup('foo') + ->addColumn('bad', options: ['column_visibility_groups' => ['typo']]); + + $this->expectException(\Kreyu\Bundle\DataTableBundle\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('Column "bad" references the column visibility group "typo"'); + + $builder->getDataTable(); + } + public function testGetHiddenColumns() { $dataTable = $this->createDataTableBuilder() From ff5415df0e9c2866f56144e35d12a9d7f74e90aa Mon Sep 17 00:00:00 2001 From: Alexandre Castelain Date: Fri, 17 Apr 2026 11:38:27 +0200 Subject: [PATCH 13/13] Replace TranslationExtension with NoOpTranslationExtension in tests to ensure compatibility with the lowest supported Twig version. Add stub for `|trans` and `{% trans_default_domain %}` functionality. --- ...ColumnVisibilityGroupSelectorBlockTest.php | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/tests/Unit/Twig/ColumnVisibilityGroupSelectorBlockTest.php b/tests/Unit/Twig/ColumnVisibilityGroupSelectorBlockTest.php index 828c7c46..42630695 100644 --- a/tests/Unit/Twig/ColumnVisibilityGroupSelectorBlockTest.php +++ b/tests/Unit/Twig/ColumnVisibilityGroupSelectorBlockTest.php @@ -12,11 +12,14 @@ use Kreyu\Bundle\DataTableBundle\Twig\DataTableExtension; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\FormExtension; -use Symfony\Bridge\Twig\Extension\TranslationExtension; -use Symfony\Component\Translation\Translator; use Twig\Environment; +use Twig\Extension\AbstractExtension; use Twig\Extra\Intl\IntlExtension; use Twig\Loader\FilesystemLoader; +use Twig\Node\Node; +use Twig\Token; +use Twig\TokenParser\AbstractTokenParser; +use Twig\TwigFilter; /** * Renders the real base.html.twig and asserts that the visibility group selector form @@ -76,7 +79,13 @@ private function renderSelector(array $urlQueryParameters, ?array $columnVisibil { $loader = new FilesystemLoader(__DIR__.'/../../../src/Resources/views/themes'); $twig = new Environment($loader, ['strict_variables' => false]); - $twig->addExtension(new TranslationExtension(new Translator('en'))); + + // Avoid Symfony TranslationExtension: its TranslationDefaultDomainNodeVisitor is incompatible + // with the lowest supported Twig version (raises "EmptyNode cannot have children"). + // A stub that no-ops `|trans` and `{% trans_default_domain %}` is enough for our purpose — + // we only render one block that doesn't translate anything. + $twig->addExtension(new NoOpTranslationExtension()); + $twig->addExtension(new FormExtension()); $twig->addExtension(new IntlExtension()); $twig->addExtension(new DataTableExtension( @@ -102,3 +111,39 @@ private function renderSelector(array $urlQueryParameters, ?array $columnVisibil ]); } } + +final class NoOpTranslationExtension extends AbstractExtension +{ + public function getFilters(): array + { + return [ + new TwigFilter('trans', static fn (mixed $value) => $value), + ]; + } + + public function getTokenParsers(): array + { + return [new NoOpTransDefaultDomainTokenParser()]; + } +} + +final class NoOpTransDefaultDomainTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + + while (!$stream->test(Token::BLOCK_END_TYPE)) { + $stream->next(); + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return new Node([], [], $token->getLine()); + } + + public function getTag(): string + { + return 'trans_default_domain'; + } +}