diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index e9b19432..2bbb3ae6 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -32,4 +32,3 @@ jobs: - name: Run PHPUnit run: vendor/bin/phpunit - diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 443d07a1..80f0117d 100755 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -6,6 +6,7 @@ ; return (new PhpCsFixer\Config()) + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) ->registerCustomFixers(new PhpCsFixerCustomFixers\Fixers()) ->setRules([ '@Symfony' => true, diff --git a/docs/src/docs/composer-require-version.data.js b/docs/src/docs/composer-require-version.data.js deleted file mode 100644 index 20dde068..00000000 --- a/docs/src/docs/composer-require-version.data.js +++ /dev/null @@ -1,11 +0,0 @@ -import { major, minor } from 'semver'; - -export default { - async load() { - return await fetch('https://packagist.org/p2/kreyu/data-table-bundle.json') - .then(response => response.json()) - .then(body => body.packages['kreyu/data-table-bundle'].shift().version) - .then(version => major(version).toString() + '.' + minor(version).toString() + '.*') - ; - } -} diff --git a/docs/src/docs/installation.md b/docs/src/docs/installation.md index 79fbbec2..7434724a 100644 --- a/docs/src/docs/installation.md +++ b/docs/src/docs/installation.md @@ -1,7 +1,3 @@ - - # Installation This bundle can be installed at any moment during a project’s lifecycle. @@ -18,12 +14,10 @@ This bundle can be installed at any moment during a project’s lifecycle. Use [Composer](https://getcomposer.org/) to install the bundle: ```shell-vue -composer require kreyu/data-table-bundle:"{{ version }}" +composer require kreyu/data-table-bundle ``` -::: danger This bundle is not production ready! -It is recommended to lock the minor version, as minor versions can provide breaking changes until the stable release! -::: +> [!DANGER] This bundle is not stable yet. Use with caution. ## Enable the bundle diff --git a/src/Column/Column.php b/src/Column/Column.php index 6964ce7b..5b0b214c 100755 --- a/src/Column/Column.php +++ b/src/Column/Column.php @@ -16,8 +16,6 @@ class Column implements ColumnInterface private ?DataTableInterface $dataTable = null; private ?PropertyPathInterface $propertyPath = null; private ?PropertyPathInterface $sortPropertyPath = null; - private int $priority = 0; - private bool $visible = true; public function __construct( private readonly ColumnConfigInterface $config, @@ -107,28 +105,4 @@ public function createExportValueView(?ValueRowView $parent = null): ColumnValue return $view; } - - public function getPriority(): int - { - return $this->priority; - } - - public function setPriority(int $priority): static - { - $this->priority = $priority; - - return $this; - } - - public function isVisible(): bool - { - return $this->visible; - } - - public function setVisible(bool $visible): static - { - $this->visible = $visible; - - return $this; - } } diff --git a/src/Column/ColumnBuilder.php b/src/Column/ColumnBuilder.php index 54d1eb76..ada9634b 100755 --- a/src/Column/ColumnBuilder.php +++ b/src/Column/ColumnBuilder.php @@ -8,59 +8,13 @@ class ColumnBuilder extends ColumnConfigBuilder implements ColumnBuilderInterface { - private int $priority = 0; - private bool $visible = true; - - public function getPriority(): int - { - if ($this->locked) { - throw $this->createBuilderLockedException(); - } - - return $this->priority; - } - - public function setPriority(int $priority): static - { - if ($this->locked) { - throw $this->createBuilderLockedException(); - } - - $this->priority = $priority; - - return $this; - } - - public function isVisible(): bool - { - if ($this->locked) { - throw $this->createBuilderLockedException(); - } - - return $this->visible; - } - - public function setVisible(bool $visible): static - { - if ($this->locked) { - throw $this->createBuilderLockedException(); - } - - $this->visible = $visible; - - return $this; - } - public function getColumn(): ColumnInterface { if ($this->locked) { throw $this->createBuilderLockedException(); } - return (new Column($this->getColumnConfig())) - ->setPriority($this->getPriority()) - ->setVisible($this->isVisible()) - ; + return new Column($this->getColumnConfig()); } private function createBuilderLockedException(): BadMethodCallException diff --git a/src/Column/ColumnBuilderInterface.php b/src/Column/ColumnBuilderInterface.php index cf067a78..12e3f6ab 100755 --- a/src/Column/ColumnBuilderInterface.php +++ b/src/Column/ColumnBuilderInterface.php @@ -6,13 +6,5 @@ interface ColumnBuilderInterface extends ColumnConfigBuilderInterface { - public function getPriority(): int; - - public function setPriority(int $priority): static; - - public function isVisible(): bool; - - public function setVisible(bool $visible): static; - public function getColumn(): ColumnInterface; } diff --git a/src/Column/ColumnConfigBuilder.php b/src/Column/ColumnConfigBuilder.php index cabdf1d8..06050d6c 100755 --- a/src/Column/ColumnConfigBuilder.php +++ b/src/Column/ColumnConfigBuilder.php @@ -19,6 +19,8 @@ class ColumnConfigBuilder implements ColumnConfigBuilderInterface private bool $sortable = false; private bool $exportable = false; private bool $personalizable = true; + private int $priority = 0; + private bool $visible = true; private ColumnFactoryInterface $columnFactory; public function __construct( @@ -222,6 +224,38 @@ public function setPersonalizable(bool $personalizable): static return $this; } + public function getPriority(): int + { + return $this->priority; + } + + public function setPriority(int $priority): static + { + if ($this->locked) { + throw $this->createBuilderLockedException(); + } + + $this->priority = $priority; + + return $this; + } + + public function isVisible(): bool + { + return $this->visible; + } + + public function setVisible(bool $visible): static + { + if ($this->locked) { + throw $this->createBuilderLockedException(); + } + + $this->visible = $visible; + + return $this; + } + public function getColumnFactory(): ColumnFactoryInterface { if (!isset($this->columnFactory)) { diff --git a/src/Column/ColumnConfigBuilderInterface.php b/src/Column/ColumnConfigBuilderInterface.php index 76ca34fc..911f7973 100755 --- a/src/Column/ColumnConfigBuilderInterface.php +++ b/src/Column/ColumnConfigBuilderInterface.php @@ -9,23 +9,8 @@ interface ColumnConfigBuilderInterface extends ColumnConfigInterface { - /** - * @deprecated since 0.14.0, provide the name using the factory {@see ColumnFactoryInterface} "named" methods instead - */ - public function setName(string $name): static; - public function setType(ResolvedColumnTypeInterface $type): static; - /** - * @deprecated since 0.14.0, modifying the options dynamically will be removed as it creates unexpected behaviors - */ - public function setOptions(array $options): static; - - /** - * @deprecated since 0.14.0, modifying the options dynamically will be removed as it creates unexpected behaviors - */ - public function setOption(string $name, mixed $value): static; - public function setAttributes(array $attributes): static; public function setAttribute(string $name, mixed $value): static; @@ -40,6 +25,10 @@ public function setExportable(bool $exportable): static; public function setPersonalizable(bool $personalizable): static; + public function setPriority(int $priority): static; + + public function setVisible(bool $visible): static; + public function setColumnFactory(ColumnFactoryInterface $columnFactory): static; public function getColumnConfig(): ColumnConfigInterface; diff --git a/src/Column/ColumnConfigInterface.php b/src/Column/ColumnConfigInterface.php index 5644df57..cd7ae367 100755 --- a/src/Column/ColumnConfigInterface.php +++ b/src/Column/ColumnConfigInterface.php @@ -35,5 +35,9 @@ public function isExportable(): bool; public function isPersonalizable(): bool; + public function getPriority(): int; + + public function isVisible(): bool; + public function getColumnFactory(): ColumnFactoryInterface; } diff --git a/src/Column/ColumnInterface.php b/src/Column/ColumnInterface.php index cf62cd61..2e8bb492 100755 --- a/src/Column/ColumnInterface.php +++ b/src/Column/ColumnInterface.php @@ -30,12 +30,4 @@ public function createValueView(?ValueRowView $parent = null): ColumnValueView; public function createExportHeaderView(?HeaderRowView $parent = null): ColumnHeaderView; public function createExportValueView(?ValueRowView $parent = null): ColumnValueView; - - public function getPriority(): int; - - public function setPriority(int $priority): static; - - public function isVisible(): bool; - - public function setVisible(bool $visible): static; } diff --git a/src/Column/ColumnSortUrlGenerator.php b/src/Column/ColumnSortUrlGenerator.php index 250b9e3a..52455d81 100644 --- a/src/Column/ColumnSortUrlGenerator.php +++ b/src/Column/ColumnSortUrlGenerator.php @@ -40,7 +40,7 @@ public function generate(DataTableView $dataTableView, ColumnHeaderView ...$colu } // Clearing the filters should reset the pagination to the first page. - if ($dataTableView->vars['pagination_enabled']) { + if ($dataTableView->vars['pagination_enabled'] ?? false) { $parameters[$dataTableView->vars['page_parameter_name']] = 1; } diff --git a/src/Column/ColumnValueView.php b/src/Column/ColumnValueView.php index 5d1e6612..0eda55f9 100755 --- a/src/Column/ColumnValueView.php +++ b/src/Column/ColumnValueView.php @@ -25,4 +25,9 @@ public function getDataTable(): DataTableView { return $this->parent->parent; } + + public function getRowData(): mixed + { + return $this->parent->data; + } } diff --git a/src/Column/Type/AbstractDateTimeColumnType.php b/src/Column/Type/AbstractDateTimeColumnType.php new file mode 100644 index 00000000..a60cdfe4 --- /dev/null +++ b/src/Column/Type/AbstractDateTimeColumnType.php @@ -0,0 +1,72 @@ +vars = array_replace($view->vars, [ + 'format' => $options['format'], + 'timezone' => $options['timezone'], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->define('format') + ->allowedTypes('string') + ->info('A date time string format, supported by the PHP date() function - null to use default.') + ; + + $resolver->define('timezone') + ->default(null) + ->allowedTypes('null', 'bool', 'string', \DateTimeZone::class) + ->info('Target timezone - null to use the default, false to leave unchanged.') + ; + + // When exporting, we ensure the export "formatter" option is present, so the value gets pre-formatted. + $resolver->addNormalizer('export', function (Options $options, mixed $export) { + if (false === $export) { + return false; + } + + if (true === $export) { + $export = []; + } + + $export['formatter'] ??= $options['formatter'] ?? function (mixed $value) use ($options) { + if (!$value instanceof \DateTimeInterface) { + return ''; + } + + $timezone = $options['timezone']; + + if (null === $timezone) { + $timezone = date_default_timezone_get(); + } + + if (is_string($timezone)) { + $timezone = new \DateTimeZone($timezone); + } + + $dateTime = \DateTime::createFromInterface($value); + + if ($timezone instanceof \DateTimeZone) { + $dateTime->setTimezone($timezone); + } + + return $dateTime->format($options['format']); + }; + + return $export; + }); + } +} diff --git a/src/Column/Type/ActionsColumnType.php b/src/Column/Type/ActionsColumnType.php index d16d2849..e32dbe6b 100755 --- a/src/Column/Type/ActionsColumnType.php +++ b/src/Column/Type/ActionsColumnType.php @@ -10,9 +10,19 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView; use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * Represents a column that contains row actions. + * + * In most cases, it is not necessary to use this column type directly. + * Instead, use the {@see DataTableBuilderInterface::addRowAction()} method. + * If at least one row action is defined and visible, column of this type is added. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/actions + */ final class ActionsColumnType extends AbstractColumnType { public function __construct( @@ -26,9 +36,14 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a foreach ($options['actions'] as $name => $action) { $action = $this->resolveAction($name, $action, $view); - $action?->setDataTable($column->getDataTable()); - $actions[$name] = $action?->createView($view); + if (null === $action) { + continue; + } + + $action->setDataTable($column->getDataTable()); + + $actions[$name] = $action->createView($view); } $view->vars['actions'] = array_filter($actions); @@ -36,14 +51,11 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setDefaults([ - 'label' => 'Actions', - 'export' => false, - 'property_path' => false, - 'actions' => [], - ]) - ->setNormalizer('actions', function (Options $options, mixed $value) { + $resolver->define('actions') + ->default([]) + ->allowedTypes('actions', 'array[]', ActionBuilderInterface::class.'[]', ActionInterface::class.'[]') + ->info('An array of actions to render in the column.') + ->normalize(function (Options $options, mixed $value) { ($resolver = new OptionsResolver()) ->setRequired([ 'type', @@ -68,9 +80,12 @@ public function configureOptions(OptionsResolver $resolver): void return $value; }) - ->setAllowedTypes('actions', ['array[]', ActionBuilderInterface::class.'[]', ActionInterface::class.'[]']) - ->setInfo('actions', 'An array of actions configuration, which contains of their type and options.') ; + + $resolver->setDefaults([ + 'export' => false, + 'property_path' => false, + ]); } private function resolveAction( diff --git a/src/Column/Type/BooleanColumnType.php b/src/Column/Type/BooleanColumnType.php index 57b53b7a..f9f10ab3 100755 --- a/src/Column/Type/BooleanColumnType.php +++ b/src/Column/Type/BooleanColumnType.php @@ -9,6 +9,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatableInterface; +/** + * Represents a column with value displayed as "yes" or "no". + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/boolean + */ final class BooleanColumnType extends AbstractColumnType { public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void @@ -21,16 +26,18 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setDefaults([ - 'label_true' => 'Yes', - 'label_false' => 'No', - 'value_translation_domain' => 'KreyuDataTable', - ]) - ->setAllowedTypes('label_true', ['string', TranslatableInterface::class]) - ->setAllowedTypes('label_false', ['string', TranslatableInterface::class]) - ->setInfo('label_true', 'Label displayed when the value equals true.') - ->setInfo('label_false', 'Label displayed when the value equals false.') + $resolver->define('label_true') + ->default('Yes') + ->allowedTypes('string', TranslatableInterface::class) + ->info('Label displayed when the value is truthy.') ; + + $resolver->define('label_false') + ->default('No') + ->allowedTypes('string', TranslatableInterface::class) + ->info('Label displayed when the value is falsy.') + ; + + $resolver->setDefault('value_translation_domain', 'KreyuDataTable'); } } diff --git a/src/Column/Type/CheckboxColumnType.php b/src/Column/Type/CheckboxColumnType.php index 7ee4af40..75ef6256 100755 --- a/src/Column/Type/CheckboxColumnType.php +++ b/src/Column/Type/CheckboxColumnType.php @@ -7,8 +7,18 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnHeaderView; use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; +use Kreyu\Bundle\DataTableBundle\DataTableBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * Represents a column with checkboxes, one in its header, and one as its value. + * + * In most cases, it is not necessary to use this column type directly. + * Instead, use the {@see DataTableBuilderInterface::addBatchAction()} method. + * If at least one batch action is defined and visible, column of this type is added. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/checkbox + */ final class CheckboxColumnType extends AbstractColumnType { public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column, array $options): void @@ -23,10 +33,15 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a public function configureOptions(OptionsResolver $resolver): void { + $resolver->define('identifier_name') + ->default('id') + ->allowedTypes('string') + ->info('The name of the identifier property.') + ; + $resolver->setDefaults([ 'label' => '□', 'property_path' => 'id', - 'identifier_name' => 'id', ]); } } diff --git a/src/Column/Type/CollectionColumnType.php b/src/Column/Type/CollectionColumnType.php index 92833cb9..9145c8fe 100755 --- a/src/Column/Type/CollectionColumnType.php +++ b/src/Column/Type/CollectionColumnType.php @@ -10,6 +10,11 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * Represents a column with value displayed as a list. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/collection + */ final class CollectionColumnType extends AbstractColumnType { public function buildColumn(ColumnBuilderInterface $builder, array $options): void @@ -45,15 +50,25 @@ public function buildExportValueView(ColumnValueView $view, ColumnInterface $col public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setDefaults([ - 'entry_type' => TextColumnType::class, - 'entry_options' => [], - 'separator' => ', ', - ]) - ->setAllowedTypes('entry_type', 'string') - ->setAllowedTypes('entry_options', 'array') - ->setAllowedTypes('separator', ['null', 'string']) + /* @see https://data-table-bundle.swroblewski.pl/reference/types/column/collection#entry_type */ + $resolver->define('entry_type') + ->default(TextColumnType::class) + ->info('Column type to render for each item in the collection.') + ->allowedTypes('string') + ; + + /* @see https://data-table-bundle.swroblewski.pl/reference/types/column/collection#entry_options */ + $resolver->define('entry_options') + ->default([]) + ->info('Options to pass to the column type for each item in the collection.') + ->allowedTypes('array') + ; + + /* @see https://data-table-bundle.swroblewski.pl/reference/types/column/collection#separator */ + $resolver->define('separator') + ->default(', ') + ->info('Separator to render between each item in the collection.') + ->allowedTypes('null', 'string') ; } diff --git a/src/Column/Type/ColumnType.php b/src/Column/Type/ColumnType.php index 2280a611..88a4f9c8 100755 --- a/src/Column/Type/ColumnType.php +++ b/src/Column/Type/ColumnType.php @@ -16,6 +16,11 @@ use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * Represents a base column with basic functionality, used as a parent for other column types. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/collection + */ final class ColumnType implements ColumnTypeInterface { public function __construct( @@ -73,33 +78,34 @@ public function buildHeaderView(ColumnHeaderView $view, ColumnInterface $column, public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void { - $rowData = $view->parent->data; + $valueRowView = $view->parent; + $dataTableView = $valueRowView->parent; + + $rowData = $view->getRowData(); - $normData = $this->getNormDataFromRowData($rowData, $column, $options); - $viewData = $this->getViewDataFromNormData($normData, $rowData, $column, $options); + $data = $this->getColumnDataFromRowData($rowData, $column, $options); + $value = $this->getColumnValueFromColumnData($data, $rowData, $column, $options); - $view->data = $normData; - $view->value = $viewData; + $view->data = $data; + $view->value = $value; if (is_callable($attr = $options['value_attr'])) { - $attr = $attr($normData, $rowData); + $attr = $attr($data, $rowData); } - $translationParameters = $options['value_translation_parameters']; - - if (is_callable($translationParameters)) { - $translationParameters = $translationParameters($normData, $rowData); + if (is_callable($translationParameters = $options['value_translation_parameters'])) { + $translationParameters = $translationParameters($data, $rowData); } $view->vars = array_replace($view->vars, [ 'name' => $column->getName(), 'column' => $view, - 'row' => $view->parent, - 'data_table' => $view->parent->parent, + 'row' => $valueRowView, + 'data_table' => $dataTableView, 'block_prefixes' => $this->getColumnBlockPrefixes($column, $options), - 'data' => $view->data, - 'value' => $view->value, - 'translation_domain' => $options['value_translation_domain'] ?? $view->parent->parent->vars['translation_domain'] ?? null, + 'data' => $data, + 'value' => $value, + 'translation_domain' => $options['value_translation_domain'] ?? $dataTableView->vars['translation_domain'] ?? null, 'translation_parameters' => $translationParameters ?? [], 'attr' => $attr, ]); @@ -162,29 +168,29 @@ public function buildExportValueView(ColumnValueView $view, ColumnInterface $col $rowData = $view->parent->data; - $normData = $this->getNormDataFromRowData($rowData, $column, $options['export']); - $viewData = $this->getViewDataFromNormData($normData, $rowData, $column, $options['export']); + $data = $this->getColumnDataFromRowData($rowData, $column, $options['export']); + $value = $this->getColumnValueFromColumnData($data, $rowData, $column, $options['export']); - if ($this->translator && (is_string($viewData) || $viewData instanceof TranslatableInterface)) { - if ($viewData instanceof TranslatableInterface) { + if ($this->translator && (is_string($value) || $value instanceof TranslatableInterface)) { + if ($value instanceof TranslatableInterface) { $locale = null; if (method_exists(TranslatableInterface::class, 'getLocale')) { $locale = $this->translator->getLocale(); } - $viewData = $viewData->trans($this->translator, $locale); + $value = $value->trans($this->translator, $locale); } else { $translationDomain = $options['export']['value_translation_domain']; $translationParameters = $options['export']['value_translation_parameters']; if (is_callable($translationParameters)) { - $translationParameters = $translationParameters($normData, $rowData); + $translationParameters = $translationParameters($data, $rowData); } if ($translationDomain) { - $viewData = $this->translator->trans( - id: $viewData, + $value = $this->translator->trans( + id: $value, parameters: $translationParameters, domain: $translationDomain, ); @@ -192,54 +198,112 @@ public function buildExportValueView(ColumnValueView $view, ColumnInterface $col } } - $view->data = $normData; - $view->value = $viewData; - - $view->vars['data'] = $normData; - $view->vars['value'] = $viewData; + $view->vars['data'] = $view->data = $data; + $view->vars['value'] = $view->value = $value; } public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setDefaults([ - 'label' => null, - 'header_translation_domain' => null, - 'header_translation_parameters' => [], - 'value_translation_domain' => false, - 'value_translation_parameters' => [], - 'block_name' => null, - 'block_prefix' => null, - 'sort' => false, - 'export' => false, - 'formatter' => null, - 'property_path' => null, - 'property_accessor' => PropertyAccess::createPropertyAccessor(), - 'getter' => null, - 'header_attr' => [], - 'value_attr' => [], - 'priority' => 0, - 'visible' => true, - 'personalizable' => true, - ]) - ->setAllowedTypes('label', ['null', 'string', TranslatableInterface::class]) - ->setAllowedTypes('header_translation_domain', ['null', 'bool', 'string']) - ->setAllowedTypes('header_translation_parameters', ['null', 'array']) - ->setAllowedTypes('value_translation_domain', ['null', 'bool', 'string']) - ->setAllowedTypes('value_translation_parameters', ['array', 'callable']) - ->setAllowedTypes('block_name', ['null', 'string']) - ->setAllowedTypes('block_prefix', ['null', 'string']) - ->setAllowedTypes('sort', ['bool', 'string']) - ->setAllowedTypes('export', ['bool', 'array']) - ->setAllowedTypes('formatter', ['null', 'callable']) - ->setAllowedTypes('property_path', ['null', 'bool', 'string', PropertyPathInterface::class]) - ->setAllowedTypes('property_accessor', PropertyAccessorInterface::class) - ->setAllowedTypes('getter', ['null', 'callable']) - ->setAllowedTypes('header_attr', 'array') - ->setAllowedTypes('value_attr', ['array', 'callable']) - ->setAllowedTypes('priority', 'int') - ->setAllowedTypes('visible', 'bool') - ->setAllowedTypes('personalizable', 'bool') + $resolver->define('label') + ->default(null) + ->allowedTypes('null', 'string', TranslatableInterface::class) + ->info('Label displayed in column header - null to default to sentence cased column name.') + ; + + $resolver->define('header_translation_domain') + ->default(null) + ->allowedTypes('null', 'bool', 'string') + ->info('Translation domain used to translate the column header - set to false to disable translation.') + ; + + $resolver->define('header_translation_parameters') + ->default([]) + ->allowedTypes('null', 'array') + ->info('Translation parameters used to translate the column header.') + ; + + $resolver->define('value_translation_domain') + ->default(false) + ->allowedTypes('null', 'bool', 'string') + ->info('Translation parameters used to translate the column value - set to false to disable translation.') + ; + + $resolver->define('value_translation_parameters') + ->default([]) + ->allowedTypes('array', 'callable') + ->info('Translation parameters used to translate the column value.') + ; + + $resolver->define('block_prefix') + ->default(null) + ->allowedTypes('null', 'string') + ->info('Defines custom block prefix to use when rendering the column.') + ; + + $resolver->define('sort') + ->default(false) + ->allowedTypes('bool', 'string') + ->info('Defines whether the column is sortable. Passing a string sets the sorting path.') + ; + + $resolver->define('export') + ->default(false) + ->allowedTypes('bool', 'array') + ->info('Defines whether the column is exportable. You can pass an array of options to differentiate them during an export.') + ; + + $resolver->define('formatter') + ->default(null) + ->allowedTypes('null', 'callable') + ->info('Formatter to use on non-empty value to customize it even further before rendering. Column value and row data are passed as arguments.') + ; + + $resolver->define('property_path') + ->default(null) + ->allowedTypes('null', 'bool', 'string', PropertyPathInterface::class) + ->info('Path to use by property accessor component to retrieve the column value from row data. Defaults to column name.') + ; + + $resolver->define('property_accessor') + ->default(PropertyAccess::createPropertyAccessor()) + ->allowedTypes(PropertyAccessorInterface::class) + ->info('An instance of property accessor to use to retrieve the value.') + ; + + $resolver->define('getter') + ->default(null) + ->allowedTypes('null', 'callable') + ->info('Callable used to retrieve column value from row data. If set, it is used instead of property accessor.') + ; + + $resolver->define('header_attr') + ->default([]) + ->allowedTypes('array') + ->info('Extra HTML attributes to render on the column header.') + ; + + $resolver->define('value_attr') + ->default([]) + ->allowedTypes('array', 'callable') + ->info('Extra HTML attributes to render on the column value.') + ; + + $resolver->define('priority') + ->default(0) + ->allowedTypes('int') + ->info('Defines the priority of the column - the higher the priority, the earlier the column will be rendered.') + ; + + $resolver->define('visible') + ->default(true) + ->allowedTypes('bool') + ->info('Defines the visibility of the column.') + ; + + $resolver->define('personalizable') + ->default(true) + ->allowedTypes('bool') + ->info('Defines whether the column can be personalized by the user in personalization feature.') ; } @@ -253,14 +317,7 @@ public function getParent(): ?string return null; } - /** - * Retrieves the column norm data from the row data by either:. - * - * - using the "getter" option; - * - using the property accessor with the "property_path" option; - * - falling back to the unmodified column data; - */ - private function getNormDataFromRowData(mixed $rowData, ColumnInterface $column, array $options): mixed + private function getColumnDataFromRowData(mixed $rowData, ColumnInterface $column, array $options): mixed { if (null === $rowData) { return null; @@ -279,29 +336,26 @@ private function getNormDataFromRowData(mixed $rowData, ColumnInterface $column, return $rowData; } - /** - * Retrieves the column view data from the norm data by applying the formatter if given. - */ - private function getViewDataFromNormData(mixed $normData, mixed $rowData, ColumnInterface $column, array $options): mixed + private function getColumnValueFromColumnData(mixed $data, mixed $rowData, ColumnInterface $column, array $options): mixed { - if (null === $normData) { + if (null === $data) { return null; } - $viewData = $normData; + $value = $data; if (is_callable($formatter = $options['formatter'])) { - $viewData = $formatter($normData, $rowData, $column, $options); + $value = $formatter($data, $rowData, $column, $options); } - return $viewData; + return $value; } /** * Retrieves the column block prefixes, respecting the type hierarchy. * - * For example, take a look at the NumberColumnType. It is based on the TextColumnType, - * which is based on the ColumnType, therefore its block prefixes are: ["number", "text", "column"]. + * For example, take a look at the NumberColumnType. It is based on the ColumnType, + * therefore its block prefixes are: ["number", "column"]. * * @return array */ diff --git a/src/Column/Type/DateColumnType.php b/src/Column/Type/DateColumnType.php index 4bfd6399..ebc621a7 100755 --- a/src/Column/Type/DateColumnType.php +++ b/src/Column/Type/DateColumnType.php @@ -6,15 +6,17 @@ use Symfony\Component\OptionsResolver\OptionsResolver; -final class DateColumnType extends AbstractColumnType +/** + * Represents a column with value displayed as a date without time. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/date + */ +final class DateColumnType extends AbstractDateTimeColumnType { public function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefault('format', 'd.m.Y'); - } + parent::configureOptions($resolver); - public function getParent(): ?string - { - return DateTimeColumnType::class; + $resolver->setDefault('format', 'd.m.Y'); } } diff --git a/src/Column/Type/DatePeriodColumnType.php b/src/Column/Type/DatePeriodColumnType.php index 8b925259..c3f8ceec 100755 --- a/src/Column/Type/DatePeriodColumnType.php +++ b/src/Column/Type/DatePeriodColumnType.php @@ -8,10 +8,17 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; use Symfony\Component\OptionsResolver\OptionsResolver; -final class DatePeriodColumnType extends AbstractColumnType +/** + * Represents a column with value displayed as date range from date period object. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/date-period + */ +final class DatePeriodColumnType extends AbstractDateTimeColumnType { public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void { + parent::buildValueView($view, $column, $options); + $view->vars = array_replace($view->vars, [ 'separator' => $options['separator'], ]); @@ -19,15 +26,14 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setDefault('separator', ' - ') - ->setAllowedTypes('separator', ['null', 'string']) - ->setInfo('separator', 'A string used to visually separate start and end dates.') - ; - } + parent::configureOptions($resolver); - public function getParent(): ?string - { - return DateTimeColumnType::class; + $resolver->setDefault('format', 'd.m.Y'); + + $resolver->define('separator') + ->default(' - ') + ->allowedTypes('null', 'string') + ->info('A string used to visually separate start and end dates.') + ; } } diff --git a/src/Column/Type/DateTimeColumnType.php b/src/Column/Type/DateTimeColumnType.php index a08ccb6d..847f325b 100755 --- a/src/Column/Type/DateTimeColumnType.php +++ b/src/Column/Type/DateTimeColumnType.php @@ -4,56 +4,19 @@ namespace Kreyu\Bundle\DataTableBundle\Column\Type; -use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; -use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -final class DateTimeColumnType extends AbstractColumnType +/** + * Represents a column with value displayed as date and time. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/date-time + */ +final class DateTimeColumnType extends AbstractDateTimeColumnType { - public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void - { - $view->vars = array_replace($view->vars, [ - 'format' => $options['format'], - 'timezone' => $options['timezone'], - ]); - } - public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setDefaults([ - 'format' => 'd.m.Y H:i:s', - 'timezone' => null, - ]) - ->setNormalizer('export', function (Options $options, $value) { - if (true === $value) { - $value = []; - } - - if (is_array($value)) { - $value += [ - 'formatter' => static function (mixed $value, mixed $data, ColumnInterface $column): string { - if ($value instanceof \DateTimeInterface) { - return $value->format($column->getConfig()->getOption('format')); - } + parent::configureOptions($resolver); - return ''; - }, - ]; - } - - return $value; - }) - ->setAllowedTypes('format', ['string']) - ->setAllowedTypes('timezone', ['null', 'string']) - ->setInfo('format', 'A date time string format, supported by the PHP date() function.') - ->setInfo('timezone', 'A timezone used to render the date time as string.') - ; - } - - public function getParent(): ?string - { - return TextColumnType::class; + $resolver->setDefault('format', 'd.m.Y H:i:s'); } } diff --git a/src/Column/Type/EnumColumnType.php b/src/Column/Type/EnumColumnType.php index 1507b61f..4083fad8 100644 --- a/src/Column/Type/EnumColumnType.php +++ b/src/Column/Type/EnumColumnType.php @@ -8,6 +8,14 @@ use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * Represents a column with PHP enumeration as value. + * + * If Symfony Translator component is installed and the enumeration + * implements the {@see TranslatableInterface}, it will be translated. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/enum + */ final class EnumColumnType extends AbstractColumnType { public function __construct( @@ -28,9 +36,4 @@ protected function format(\UnitEnum $enum): string return $enum->name; } - - public function getParent(): ?string - { - return TextColumnType::class; - } } diff --git a/src/Column/Type/FormColumnType.php b/src/Column/Type/FormColumnType.php deleted file mode 100755 index 1ca401ed..00000000 --- a/src/Column/Type/FormColumnType.php +++ /dev/null @@ -1,36 +0,0 @@ -vars = array_replace($view->vars, [ - 'form' => $options['form'], - 'form_child_path' => $options['form_child_path'] ?? $column->getName(), - ]); - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver - ->setRequired('form') - ->setDefault('form_child_path', null) - ->setAllowedTypes('form', FormInterface::class) - ->setAllowedTypes('form_child_path', ['null', 'bool', 'string']) - ->setInfo('form', 'An instance of the form which wraps the data table.') - ->setInfo('form_child_path', 'A path to the child form of each collection field.') - ; - } -} diff --git a/src/Column/Type/HtmlColumnType.php b/src/Column/Type/HtmlColumnType.php index 31407b5d..8498f438 100644 --- a/src/Column/Type/HtmlColumnType.php +++ b/src/Column/Type/HtmlColumnType.php @@ -8,6 +8,11 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * Represents a column with value displayed as HTML. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/html + */ final class HtmlColumnType extends AbstractColumnType { public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void diff --git a/src/Column/Type/IconColumnType.php b/src/Column/Type/IconColumnType.php index b1205796..6ccea4b9 100644 --- a/src/Column/Type/IconColumnType.php +++ b/src/Column/Type/IconColumnType.php @@ -8,6 +8,9 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/icon + */ final class IconColumnType extends AbstractColumnType { public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void diff --git a/src/Column/Type/LinkColumnType.php b/src/Column/Type/LinkColumnType.php index 2ed60c3b..3b9a305f 100755 --- a/src/Column/Type/LinkColumnType.php +++ b/src/Column/Type/LinkColumnType.php @@ -8,6 +8,14 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * Represents a column with value displayed as a link. + * + * When using Turbo v8+, by default, the link will be prefetched on hover. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/link + * @see https://turbo.hotwired.dev/handbook/drive#prefetching-links-on-hover + */ final class LinkColumnType extends AbstractColumnType { public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void @@ -28,18 +36,16 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setDefaults([ - 'href' => '#', - 'target' => null, - ]) - ->setAllowedTypes('href', ['string', 'callable']) - ->setAllowedTypes('target', ['null', 'string', 'callable']) + $resolver->define('href') + ->default('#') + ->allowedTypes('string', 'callable') + ->info('Defines the URL to link to.') ; - } - public function getParent(): ?string - { - return TextColumnType::class; + $resolver->define('target') + ->default(null) + ->allowedTypes('null', 'string', 'callable') + ->info('Sets the value that will be used as a "target" HTML attribute.') + ; } } diff --git a/src/Column/Type/MoneyColumnType.php b/src/Column/Type/MoneyColumnType.php index 0dca8114..df32551c 100755 --- a/src/Column/Type/MoneyColumnType.php +++ b/src/Column/Type/MoneyColumnType.php @@ -7,7 +7,13 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnInterface; use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Translation\Formatter\IntlFormatter; +/** + * Represents a column with monetary value, appropriately formatted and rendered with currency. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/money + */ final class MoneyColumnType extends AbstractColumnType { public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void @@ -16,29 +22,48 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a $currency = $currency($view->parent->data); } - if (1 !== $options['divisor']) { - $view->vars['value'] /= $options['divisor']; + if (null !== $divisor = $options['divisor']) { + $view->vars['value'] /= $divisor; } $view->vars = array_merge($view->vars, [ 'currency' => $currency, + 'divisor' => $divisor, + 'use_intl_formatter' => $options['use_intl_formatter'], + 'intl_formatter_options' => $options['intl_formatter_options'], ]); } public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setRequired(['currency', 'divisor']) - ->setAllowedTypes('currency', ['string', 'callable']) - ->setAllowedTypes('divisor', 'int') + $resolver->define('currency') + ->required() + ->allowedTypes('string', 'callable') ; - $resolver->setDefaults([ - 'divisor' => 1, - ]); - } - public function getParent(): ?string - { - return NumberColumnType::class; + $resolver->define('divisor') + ->default(null) + ->allowedTypes('null', 'int') + ->allowedValues(fn (?int $value) => 0 !== $value) + ->info('A divisor used to divide the value before rendering.') + ; + + $resolver->define('use_intl_formatter') + ->default(class_exists(IntlFormatter::class)) + ->allowedTypes('bool') + ; + + $resolver->define('intl_formatter_options') + ->default(function (OptionsResolver $resolver) { + $resolver + ->setDefaults([ + 'attrs' => [], + 'style' => 'decimal', + ]) + ->setAllowedTypes('attrs', 'array') + ->setAllowedTypes('style', 'string') + ; + }) + ; } } diff --git a/src/Column/Type/NumberColumnType.php b/src/Column/Type/NumberColumnType.php index e1fadc3b..55fbcea6 100755 --- a/src/Column/Type/NumberColumnType.php +++ b/src/Column/Type/NumberColumnType.php @@ -9,6 +9,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Translation\Formatter\IntlFormatter; +/** + * Represents a column with value displayed as a number. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/number + */ final class NumberColumnType extends AbstractColumnType { public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void @@ -21,26 +26,22 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setDefaults([ - 'use_intl_formatter' => class_exists(IntlFormatter::class), - 'intl_formatter_options' => function (OptionsResolver $resolver) { - $resolver - ->setDefaults([ - 'attrs' => [], - 'style' => 'decimal', - ]) - ->setAllowedTypes('attrs', 'array') - ->setAllowedTypes('style', 'string') - ; - }, - ]) - ->setAllowedTypes('use_intl_formatter', 'bool') + $resolver->define('use_intl_formatter') + ->default(class_exists(IntlFormatter::class)) + ->allowedTypes('bool') ; - } - public function getParent(): ?string - { - return TextColumnType::class; + $resolver->define('intl_formatter_options') + ->default(function (OptionsResolver $resolver) { + $resolver + ->setDefaults([ + 'attrs' => [], + 'style' => 'decimal', + ]) + ->setAllowedTypes('attrs', 'array') + ->setAllowedTypes('style', 'string') + ; + }) + ; } } diff --git a/src/Column/Type/ResolvedColumnType.php b/src/Column/Type/ResolvedColumnType.php index ad5e8613..76e78d44 100755 --- a/src/Column/Type/ResolvedColumnType.php +++ b/src/Column/Type/ResolvedColumnType.php @@ -24,9 +24,9 @@ class ResolvedColumnType implements ResolvedColumnTypeInterface * @param array $typeExtensions */ public function __construct( - private readonly ColumnTypeInterface $innerType, - private readonly array $typeExtensions = [], - private readonly ?ResolvedColumnTypeInterface $parent = null, + private ColumnTypeInterface $innerType, + private array $typeExtensions = [], + private ?ResolvedColumnTypeInterface $parent = null, ) { } diff --git a/src/Column/Type/TemplateColumnType.php b/src/Column/Type/TemplateColumnType.php index df80f5d2..cc95ff5c 100755 --- a/src/Column/Type/TemplateColumnType.php +++ b/src/Column/Type/TemplateColumnType.php @@ -8,6 +8,11 @@ use Kreyu\Bundle\DataTableBundle\Column\ColumnValueView; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * Represents a column rendered from a Twig template. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/template + */ final class TemplateColumnType extends AbstractColumnType { public function buildValueView(ColumnValueView $view, ColumnInterface $column, array $options): void @@ -28,17 +33,16 @@ public function buildValueView(ColumnValueView $view, ColumnInterface $column, a public function configureOptions(OptionsResolver $resolver): void { - $resolver - ->setRequired([ - 'template_path', - ]) - ->setDefaults([ - 'template_vars' => [], - ]) - ->setAllowedTypes('template_path', ['string', 'callable']) - ->setAllowedTypes('template_vars', ['array', 'callable']) - ->setInfo('template_path', 'A path to the template that should be rendered.') - ->setInfo('template_vars', 'An array of variables passed to the template.') + $resolver->define('template_path') + ->required() + ->allowedTypes('string', 'callable') + ->info('A path to the template that should be rendered.') + ; + + $resolver->define('template_vars') + ->default([]) + ->allowedTypes('array', 'callable') + ->info('An array of variables passed to the template.') ; } } diff --git a/src/Column/Type/TextColumnType.php b/src/Column/Type/TextColumnType.php index 8704f364..e5bcab11 100755 --- a/src/Column/Type/TextColumnType.php +++ b/src/Column/Type/TextColumnType.php @@ -4,7 +4,11 @@ namespace Kreyu\Bundle\DataTableBundle\Column\Type; +/** + * Represents a column with value displayed as a text. + * + * @see https://data-table-bundle.swroblewski.pl/reference/types/column/text + */ final class TextColumnType extends AbstractColumnType { - // ... } diff --git a/src/DataTable.php b/src/DataTable.php index 6423d43f..be656b1a 100755 --- a/src/DataTable.php +++ b/src/DataTable.php @@ -172,37 +172,92 @@ public function getConfig(): DataTableConfigInterface public function getColumns(): array { - $columns = $this->columns; + uasort($this->columns, function (ColumnInterface $columnA, ColumnInterface $columnB): int { + $priorityA = $columnA->getConfig()->getPriority(); + $priorityB = $columnB->getConfig()->getPriority(); + + if ($this->getConfig()->isPersonalizationEnabled()) { + if ($columnA->getConfig()->isPersonalizable()) { + $priorityA = $this->personalizationData?->getColumn($columnA)?->getPriority() ?? $priorityA; + } + + if ($columnB->getConfig()->isPersonalizable()) { + $priorityB = $this->personalizationData?->getColumn($columnB)?->getPriority() ?? $priorityB; + } + } - uasort($columns, static function (ColumnInterface $columnA, ColumnInterface $columnB): int { - return $columnB->getPriority() <=> $columnA->getPriority(); + return $priorityB <=> $priorityA; }); - return $columns; + return $this->columns; } public function getVisibleColumns(): array { - return array_filter( - $this->getColumns(), - static fn (ColumnInterface $column) => $column->isVisible(), - ); + return array_filter($this->getColumns(), function (ColumnInterface $column) { + $visible = $column->getConfig()->isVisible(); + + if ($this->getConfig()->isPersonalizationEnabled() && $column->getConfig()->isPersonalizable()) { + $visible = $this->personalizationData?->getColumn($column)?->isVisible() ?? $visible; + } + + return $visible; + }); } public function getHiddenColumns(): array { - return array_filter( - $this->getColumns(), - static fn (ColumnInterface $column) => !$column->isVisible(), - ); + return array_filter($this->getColumns(), function (ColumnInterface $column) { + $visible = $column->getConfig()->isVisible(); + + if ($this->getConfig()->isPersonalizationEnabled() && $column->getConfig()->isPersonalizable()) { + $visible = $this->personalizationData?->getColumn($column)?->isVisible() ?? $visible; + } + + return !$visible; + }); } public function getExportableColumns(): array { - return array_filter( - $this->getVisibleColumns(), - static fn (ColumnInterface $column) => $column->getConfig()->isExportable(), - ); + $columns = array_filter($this->columns, function (ColumnInterface $column) { + if (!$column->getConfig()->isExportable()) { + return false; + } + + $exportOptions = $column->getConfig()->getOption('export') ?: []; + + $columnPersonalizable = $exportOptions['personalizable'] ?? $column->getConfig()->isPersonalizable(); + $columnVisible = $exportOptions['visible'] ?? $column->getConfig()->isVisible(); + + if ($this->getConfig()->isPersonalizationEnabled() && $columnPersonalizable) { + $columnVisible = $this->personalizationData?->getColumn($column)?->isVisible() ?? $columnVisible; + } + + return $columnVisible; + }); + + uasort($columns, function (ColumnInterface $columnA, ColumnInterface $columnB): int { + $exportOptionsA = $columnA->getConfig()->getOption('export') ?: []; + $exportOptionsB = $columnB->getConfig()->getOption('export') ?: []; + + $priorityA = $exportOptionsA['priority'] ?? $columnA->getConfig()->getPriority(); + $priorityB = $exportOptionsB['priority'] ?? $columnB->getConfig()->getPriority(); + + if ($this->getConfig()->isPersonalizationEnabled()) { + if ($exportOptionsA['personalizable'] ?? $columnA->getConfig()->isPersonalizable()) { + $priorityA = $this->personalizationData?->getColumn($columnA)?->getPriority() ?? $priorityA; + } + + if ($exportOptionsB['personalizable'] ?? $columnB->getConfig()->isPersonalizable()) { + $priorityB = $this->personalizationData?->getColumn($columnB)?->getPriority() ?? $priorityB; + } + } + + return $priorityB <=> $priorityA; + }); + + return $columns; } public function getColumn(string $name): ColumnInterface @@ -561,8 +616,6 @@ public function personalize(PersonalizationData $data, bool $persistence = true) $this->setPersonalizationData($data); - $data->apply($this->getColumns()); - $this->dispatch(DataTableEvents::POST_PERSONALIZE, new DataTablePersonalizationEvent($this, $data)); } @@ -588,7 +641,7 @@ public function export(?ExportData $data = null): ExportFile } if (!$data->includePersonalization) { - $dataTable->resetPersonalization(); + $this->personalizationData = null; } if (null === $data->exporter) { @@ -962,16 +1015,4 @@ private function getPersistenceSubject(PersistenceContext $context): Persistence return $provider->provide(); } - - private function resetPersonalization(): void - { - $this->personalizationData = null; - - foreach ($this->columns as $column) { - $column - ->setPriority($column->getConfig()->getOption('priority')) - ->setVisible($column->getConfig()->getOption('visible')) - ; - } - } } diff --git a/src/Filter/FilterClearUrlGenerator.php b/src/Filter/FilterClearUrlGenerator.php index 762e95fa..aff9caa3 100644 --- a/src/Filter/FilterClearUrlGenerator.php +++ b/src/Filter/FilterClearUrlGenerator.php @@ -37,7 +37,7 @@ public function generate(DataTableView $dataTableView, FilterView ...$filterView } // Clearing the filters should reset the pagination to the first page. - if ($dataTableView->vars['pagination_enabled']) { + if ($dataTableView->vars['pagination_enabled'] ?? false) { $parameters[$dataTableView->vars['page_parameter_name']] = 1; } diff --git a/src/Personalization/PersonalizationColumnData.php b/src/Personalization/PersonalizationColumnData.php index 60905ff1..ea16b8fb 100755 --- a/src/Personalization/PersonalizationColumnData.php +++ b/src/Personalization/PersonalizationColumnData.php @@ -47,8 +47,8 @@ public static function fromColumn(ColumnInterface $column): self { return new self( $column->getName(), - $column->getPriority(), - $column->isVisible(), + $column->getConfig()->getPriority(), + $column->getConfig()->isVisible(), ); } diff --git a/src/Personalization/PersonalizationData.php b/src/Personalization/PersonalizationData.php index 9b54524b..d037aae7 100755 --- a/src/Personalization/PersonalizationData.php +++ b/src/Personalization/PersonalizationData.php @@ -69,26 +69,6 @@ public static function fromDataTable(DataTableInterface $dataTable): self )); } - /** - * @param array $columns - */ - public function apply(array $columns): void - { - foreach ($columns as $column) { - if (!$column->getConfig()->isPersonalizable()) { - continue; - } - - if (null === $data = $this->getColumn($column)) { - continue; - } - - $column - ->setPriority($data->getPriority()) - ->setVisible($data->isVisible()); - } - } - public function getColumns(): array { return $this->columns; diff --git a/src/Resources/config/columns.php b/src/Resources/config/columns.php index 039c72b8..18dd063f 100755 --- a/src/Resources/config/columns.php +++ b/src/Resources/config/columns.php @@ -17,7 +17,6 @@ use Kreyu\Bundle\DataTableBundle\Column\Type\DatePeriodColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\DateTimeColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\EnumColumnType; -use Kreyu\Bundle\DataTableBundle\Column\Type\FormColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\HtmlColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\IconColumnType; use Kreyu\Bundle\DataTableBundle\Column\Type\LinkColumnType; @@ -86,11 +85,6 @@ ->tag('kreyu_data_table.column.type') ; - $services - ->set('kreyu_data_table.column.type.form', FormColumnType::class) - ->tag('kreyu_data_table.column.type') - ; - $services ->set('kreyu_data_table.column.type.link', LinkColumnType::class) ->tag('kreyu_data_table.column.type') diff --git a/src/Test/Column/Type/ColumnTypeTestCase.php b/src/Test/Column/Type/ColumnTypeTestCase.php index df96e822..6dcf33f4 100644 --- a/src/Test/Column/Type/ColumnTypeTestCase.php +++ b/src/Test/Column/Type/ColumnTypeTestCase.php @@ -83,8 +83,8 @@ protected function createColumnRegistry(): ColumnRegistryInterface { return new ColumnRegistry( types: [ - $this->getTestedColumnType(), ...$this->getAdditionalColumnTypes(), + $this->getTestedColumnType(), ], typeExtensions: [], resolvedTypeFactory: $this->getResolvedColumnTypeFactory(), diff --git a/src/Test/DataTableIntegrationTestCase.php b/src/Test/DataTableIntegrationTestCase.php new file mode 100644 index 00000000..c804a2aa --- /dev/null +++ b/src/Test/DataTableIntegrationTestCase.php @@ -0,0 +1,109 @@ +dataTableFactory = $this->createDataTableFactory(); + } + + protected function createDataTableFactory(): DataTableFactoryInterface + { + return new DataTableFactory($this->createDataTableRegistry()); + } + + protected function createDataTableRegistry(): DataTableRegistry + { + return new DataTableRegistry( + types: $this->getDataTableTypes(), + typeExtensions: $this->getDataTableTypeExtensions(), + proxyQueryFactories: $this->getProxyQueryFactories(), + resolvedTypeFactory: $this->getResolvedDataTableTypeFactory(), + ); + } + + /** + * @return DataTableTypeInterface[] + */ + protected function getDataTableTypes(): array + { + return [ + new DataTableType([ + 'column_factory' => $this->createColumnFactory(), + ]), + ]; + } + + protected function getDataTableTypeExtensions(): array + { + return []; + } + + protected function getProxyQueryFactories(): array + { + return [ + new ArrayProxyQueryFactory(), + ]; + } + + protected function getResolvedDataTableTypeFactory(): ResolvedDataTableTypeFactoryInterface + { + return new ResolvedDataTableTypeFactory(); + } + + protected function createColumnFactory(): ColumnFactoryInterface + { + return new ColumnFactory($this->createColumnRegistry()); + } + + protected function createColumnRegistry(): ColumnRegistryInterface + { + return new ColumnRegistry( + types: $this->getColumnTypes(), + typeExtensions: $this->getColumnTypeExtensions(), + resolvedTypeFactory: $this->getResolvedColumnTypeFactory(), + ); + } + + protected function getColumnTypes(): array + { + return [ + new ColumnType(), + new TextColumnType(), + ]; + } + + protected function getColumnTypeExtensions(): array + { + return []; + } + + protected function getResolvedColumnTypeFactory(): ResolvedColumnTypeFactoryInterface + { + return new ResolvedColumnTypeFactory(); + } +} diff --git a/src/Test/DataTableTypeTestCase.php b/src/Test/DataTableTypeTestCase.php new file mode 100644 index 00000000..76cd5241 --- /dev/null +++ b/src/Test/DataTableTypeTestCase.php @@ -0,0 +1,32 @@ + + */ + abstract protected function getTestedType(): string; + + protected function createDataTable(array $options = []): DataTableInterface + { + return $this->dataTableFactory->create($this->getTestedType(), $options); + } + + protected function createNamedDataTable(string $name, array $options = []): DataTableInterface + { + return $this->dataTableFactory->createNamed($name, $this->getTestedType(), $options); + } + + protected function createDataTableView(DataTableInterface $dataTable): DataTableView + { + return $dataTable->createView(); + } +} diff --git a/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryTest.php b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryTest.php index e6fd5f9c..2bf7789d 100644 --- a/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryTest.php +++ b/tests/Unit/Bridge/Doctrine/Orm/Filter/ExpressionFactory/ExpressionFactoryTest.php @@ -45,7 +45,7 @@ public function testItCreatesExpression(string $queryPath, FilterData $data, arr public static function expectedExpressionProvider(): iterable { yield 'equals' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::Equals), 'parameters' => [ new Parameter('bar', null), @@ -54,7 +54,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'not equals' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::NotEquals), 'parameters' => [ new Parameter('bar', null), @@ -63,7 +63,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'contains' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::Contains), 'parameters' => [ new Parameter('bar', null), @@ -72,7 +72,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'not contains' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::NotContains), 'parameters' => [ new Parameter('bar', null), @@ -81,7 +81,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'in' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::In), 'parameters' => [ new Parameter('bar', null), @@ -90,7 +90,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'not in' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::NotIn), 'parameters' => [ new Parameter('bar', null), @@ -99,7 +99,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'greater than' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::GreaterThan), 'parameters' => [ new Parameter('bar', null), @@ -108,7 +108,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'greater than or equals' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::GreaterThanEquals), 'parameters' => [ new Parameter('bar', null), @@ -117,7 +117,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'less than' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::LessThan), 'parameters' => [ new Parameter('bar', null), @@ -126,7 +126,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'less than or equals' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::LessThanEquals), 'parameters' => [ new Parameter('bar', null), @@ -135,7 +135,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'starts with' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::StartsWith), 'parameters' => [ new Parameter('bar', null), @@ -144,7 +144,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'ends with' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::EndsWith), 'parameters' => [ new Parameter('bar', null), @@ -153,7 +153,7 @@ public static function expectedExpressionProvider(): iterable ]; yield 'between' => [ - 'query_path' => 'foo', + 'queryPath' => 'foo', 'data' => new FilterData(null, Operator::Between), 'parameters' => [ 'from' => new Parameter('bar', null), diff --git a/tests/Unit/Column/ColumnBuilderTest.php b/tests/Unit/Column/ColumnBuilderTest.php index ad3fab29..5580b744 100644 --- a/tests/Unit/Column/ColumnBuilderTest.php +++ b/tests/Unit/Column/ColumnBuilderTest.php @@ -16,34 +16,14 @@ class ColumnBuilderTest extends TestCase public function testGetColumn(): void { $builder = $this->createBuilder(); - $builder->setPriority(100); - $builder->setVisible(false); $config = $builder->getColumnConfig(); $column = $builder->getColumn(); $this->assertEquals($config, $column->getConfig()); - $this->assertSame(100, $column->getPriority()); - $this->assertFalse($column->isVisible()); $this->assertTrue($this->getPrivatePropertyValue($config, 'locked')); } - public function testGetPriority() - { - $builder = $this->createBuilder(); - - $this->assertEquals(0, $builder->getPriority()); - $this->assertEquals(100, $builder->setPriority(100)->getPriority()); - } - - public function testIsVisible() - { - $builder = $this->createBuilder(); - - $this->assertTrue($builder->setVisible(true)->isVisible()); - $this->assertFalse($builder->setVisible(false)->isVisible()); - } - private function createBuilder(): ColumnBuilder { return new ColumnBuilder( diff --git a/tests/Unit/Column/ColumnConfigBuilderTest.php b/tests/Unit/Column/ColumnConfigBuilderTest.php index e1fe1cf4..f1777861 100644 --- a/tests/Unit/Column/ColumnConfigBuilderTest.php +++ b/tests/Unit/Column/ColumnConfigBuilderTest.php @@ -124,6 +124,22 @@ public function testIsPersonalizable() $this->assertFalse($builder->setPersonalizable(false)->isPersonalizable()); } + public function testIsVisible() + { + $builder = $this->createBuilder(); + + $this->assertTrue($builder->setVisible(true)->isVisible()); + $this->assertFalse($builder->setVisible(false)->isVisible()); + } + + public function testGetPriority() + { + $builder = $this->createBuilder(); + $builder->setPriority(100); + + $this->assertEquals(100, $builder->getPriority()); + } + public function testGetColumnFactoryWithoutFactorySet() { $this->expectException(BadMethodCallException::class); diff --git a/tests/Unit/Column/Type/ColumnTypeTest.php b/tests/Unit/Column/Type/ColumnTypeTest.php index 8f5fce4e..857fe847 100644 --- a/tests/Unit/Column/Type/ColumnTypeTest.php +++ b/tests/Unit/Column/Type/ColumnTypeTest.php @@ -31,11 +31,6 @@ protected function getTestedColumnType(): ColumnTypeInterface return new ColumnType($this->translator); } - protected function getAdditionalColumnTypes(): array - { - return []; - } - public function testDefaultLabelInheritsFromName(): void { $column = $this->createNamedColumn('firstName'); @@ -795,7 +790,7 @@ public function testPassingPriorityOption(): void 'priority' => 10, ]); - $this->assertEquals(10, $column->getPriority()); + $this->assertEquals(10, $column->getConfig()->getPriority()); } public function testPassingVisibleOption(): void @@ -804,7 +799,7 @@ public function testPassingVisibleOption(): void 'visible' => false, ]); - $this->assertFalse($column->isVisible()); + $this->assertFalse($column->getConfig()->isVisible()); } public function testPassingPersonalizableOption(): void diff --git a/tests/Unit/DataTableTest.php b/tests/Unit/DataTableTest.php new file mode 100644 index 00000000..22ef1fe4 --- /dev/null +++ b/tests/Unit/DataTableTest.php @@ -0,0 +1,361 @@ +createDataTableBuilder() + ->addColumn('first', options: ['priority' => 1]) + ->addColumn('second', options: ['priority' => 2]) + ->addColumn('third', options: ['priority' => 3]) + ->addColumn('fourth', options: ['priority' => 4]) + ->addColumn('fifth', options: ['priority' => 5]) + ->getDataTable() + ; + + $columns = array_keys($dataTable->getColumns()); + + $this->assertEquals(['fifth', 'fourth', 'third', 'second', 'first'], $columns); + } + + public function testGetColumnsRespectsPersonalization() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => true]) + ->addColumn('first', options: ['priority' => 1]) + ->addColumn('second', options: ['priority' => 2]) + ->addColumn('third', options: ['priority' => 3]) + ->addColumn('fourth', options: ['priority' => 4]) + ->addColumn('fifth', options: ['priority' => 100, 'personalizable' => false]) + ->getDataTable(); + + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'first' => ['priority' => 5], + 'second' => ['priority' => 4], + 'third' => ['priority' => 3], + 'fourth' => ['priority' => 2], + // Should be ignored, because column has "personalizable" option set to false + 'fifth' => ['priority' => 1], + ], + ])); + + $columns = array_keys($dataTable->getColumns()); + + $this->assertEquals(['fifth', 'first', 'second', 'third', 'fourth'], $columns); + } + + public function testGetColumnsIgnoresDisabledPersonalization() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => false]) + ->addColumn('first', options: ['priority' => 1]) + ->addColumn('second', options: ['priority' => 2]) + ->addColumn('third', options: ['priority' => 3]) + ->addColumn('fourth', options: ['priority' => 4]) + ->addColumn('fifth', options: ['priority' => 5]) + ->getDataTable(); + + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'first' => ['priority' => 5], + 'second' => ['priority' => 4], + 'third' => ['priority' => 3], + 'fourth' => ['priority' => 2], + 'fifth' => ['priority' => 1], + ], + ])); + + $columns = array_keys($dataTable->getColumns()); + + $this->assertEquals(['fifth', 'fourth', 'third', 'second', 'first'], $columns); + } + + public function testGetVisibleColumns() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('first', options: ['priority' => 1, 'visible' => true]) + ->addColumn('second', options: ['priority' => 2, 'visible' => false]) + ->addColumn('third', options: ['priority' => 3, 'visible' => true]) + ->addColumn('fourth', options: ['priority' => 4, 'visible' => false]) + ->addColumn('fifth', options: ['priority' => 5, 'visible' => true]) + ->getDataTable(); + + $columns = array_keys($dataTable->getVisibleColumns()); + + $this->assertEquals(['fifth', 'third', 'first'], $columns); + } + + public function testGetVisibleColumnsRespectsPersonalization() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => true]) + ->addColumn('first', options: ['priority' => 1, 'visible' => true]) + ->addColumn('second', options: ['priority' => 2, 'visible' => false]) + ->addColumn('third', options: ['priority' => 3, 'visible' => true]) + ->addColumn('fourth', options: ['priority' => 4, 'visible' => false]) + ->addColumn('fifth', options: ['priority' => 100, 'visible' => true, 'personalizable' => false]) + ->getDataTable(); + + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'first' => ['priority' => 5, 'visible' => false], + 'second' => ['priority' => 4, 'visible' => true], + 'third' => ['priority' => 3, 'visible' => false], + 'fourth' => ['priority' => 2, 'visible' => true], + // Should be ignored, because column has "personalizable" option set to false + 'fifth' => ['priority' => 1, 'visible' => false], + ], + ])); + + $columns = array_keys($dataTable->getVisibleColumns()); + + $this->assertEquals(['fifth', 'second', 'fourth'], $columns); + } + + public function testGetVisibleColumnsIgnoresDisabledPersonalization() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => false]) + ->addColumn('first', options: ['priority' => 1, 'visible' => true]) + ->addColumn('second', options: ['priority' => 2, 'visible' => false]) + ->addColumn('third', options: ['priority' => 3, 'visible' => true]) + ->addColumn('fourth', options: ['priority' => 4, 'visible' => false]) + ->addColumn('fifth', options: ['priority' => 5, 'visible' => true]) + ->getDataTable(); + + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'first' => ['priority' => 5, 'visible' => false], + 'second' => ['priority' => 4, 'visible' => true], + 'third' => ['priority' => 3, 'visible' => false], + 'fourth' => ['priority' => 2, 'visible' => true], + 'fifth' => ['priority' => 1, 'visible' => false], + ], + ])); + + $columns = array_keys($dataTable->getVisibleColumns()); + + $this->assertEquals(['fifth', 'third', 'first'], $columns); + } + + public function testGetHiddenColumns() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('first', options: ['visible' => true]) + ->addColumn('second', options: ['visible' => false]) + ->addColumn('third', options: ['visible' => true]) + ->addColumn('fourth', options: ['visible' => false]) + ->addColumn('fifth', options: ['visible' => true]) + ->getDataTable(); + + $columns = array_keys($dataTable->getHiddenColumns()); + + $this->assertEquals(['second', 'fourth'], $columns); + } + + public function testGetHiddenColumnsRespectsPersonalization() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => true]) + ->addColumn('first', options: ['priority' => 1, 'visible' => true]) + ->addColumn('second', options: ['priority' => 2, 'visible' => false]) + ->addColumn('third', options: ['priority' => 3, 'visible' => true]) + ->addColumn('fourth', options: ['priority' => 100, 'visible' => false, 'personalizable' => false]) + ->addColumn('fifth', options: ['priority' => 5, 'visible' => true]) + ->getDataTable(); + + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'first' => ['priority' => 5, 'visible' => false], + 'second' => ['priority' => 4, 'visible' => true], + 'third' => ['priority' => 3, 'visible' => false], + // Should be ignored, because column has "personalizable" option set to false + 'fourth' => ['priority' => 2, 'visible' => true], + 'fifth' => ['priority' => 1, 'visible' => false], + ], + ])); + + $columns = array_keys($dataTable->getHiddenColumns()); + + $this->assertEquals(['fourth', 'first', 'third', 'fifth'], $columns); + } + + public function testGetHiddenColumnsIgnoresDisabledPersonalization() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => false]) + ->addColumn('first', options: ['priority' => 1, 'visible' => true]) + ->addColumn('second', options: ['priority' => 2, 'visible' => false]) + ->addColumn('third', options: ['priority' => 3, 'visible' => true]) + ->addColumn('fourth', options: ['priority' => 4, 'visible' => false]) + ->addColumn('fifth', options: ['priority' => 5, 'visible' => true]) + ->getDataTable(); + + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'first' => ['priority' => 5, 'visible' => false], + 'second' => ['priority' => 4, 'visible' => true], + 'third' => ['priority' => 3, 'visible' => false], + 'fourth' => ['priority' => 2, 'visible' => true], + 'fifth' => ['priority' => 1, 'visible' => false], + ], + ])); + + $columns = array_keys($dataTable->getHiddenColumns()); + + $this->assertEquals(['fourth', 'second'], $columns); + } + + public function testGetExportableColumns() + { + $dataTable = $this->createDataTableBuilder() + ->addColumn('first', options: [ + 'priority' => 1, + 'visible' => true, + 'export' => true, + ]) + ->addColumn('second', options: [ + 'priority' => 2, + 'visible' => true, + 'export' => [ + 'visible' => false, + ], + ]) + ->addColumn('third', options: [ + 'priority' => 3, + 'visible' => true, + 'export' => [ + 'priority' => 100, + ], + ]) + ->addColumn('fourth', options: [ + 'priority' => 4, + 'visible' => false, + 'export' => [ + 'visible' => true, + ], + ]) + ->addColumn('fifth', options: [ + 'priority' => 5, + 'visible' => true, + 'export' => false, + ]) + ->getDataTable(); + + $columns = array_keys($dataTable->getExportableColumns()); + + $this->assertEquals(['third', 'fourth', 'first'], $columns); + } + + public function testGetExportableColumnsRespectsPersonalization() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => true]) + ->addColumn('first', options: [ + 'priority' => 1, + 'visible' => true, + 'export' => true, + ]) + ->addColumn('second', options: [ + 'priority' => 2, + 'visible' => true, + 'export' => [ + 'visible' => false, + ], + ]) + ->addColumn('third', options: [ + 'priority' => 3, + 'visible' => true, + 'export' => [ + 'priority' => 100, + ], + ]) + ->addColumn('fourth', options: [ + 'priority' => 4, + 'visible' => false, + 'export' => [ + 'visible' => true, + 'personalizable' => false, + ], + ]) + ->addColumn('fifth', options: [ + 'priority' => 5, + 'visible' => true, + 'export' => false, + ]) + ->getDataTable(); + + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'first' => ['priority' => 5, 'visible' => false], + 'second' => ['priority' => 4, 'visible' => true], + 'third' => ['priority' => 3, 'visible' => false], + 'fourth' => ['priority' => 2, 'visible' => false], + 'fifth' => ['priority' => 1, 'visible' => false], + ], + ])); + + $columns = array_keys($dataTable->getExportableColumns()); + + $this->assertEquals(['second', 'fourth'], $columns); + } + + public function testGetExportableColumnsIgnoresDisabledPersonalization() + { + $dataTable = $this->createDataTableBuilder(['personalization_enabled' => false]) + ->addColumn('first', options: [ + 'priority' => 1, + 'visible' => true, + 'export' => true, + ]) + ->addColumn('second', options: [ + 'priority' => 2, + 'visible' => true, + 'export' => [ + 'visible' => false, + ], + ]) + ->addColumn('third', options: [ + 'priority' => 3, + 'visible' => true, + 'export' => [ + 'priority' => 100, + ], + ]) + ->addColumn('fourth', options: [ + 'priority' => 4, + 'visible' => false, + 'export' => [ + 'visible' => true, + ], + ]) + ->addColumn('fifth', options: [ + 'priority' => 5, + 'visible' => true, + 'export' => false, + ]) + ->getDataTable(); + + $dataTable->setPersonalizationData(PersonalizationData::fromArray([ + 'columns' => [ + 'first' => ['priority' => 5, 'visible' => false], + 'second' => ['priority' => 4, 'visible' => true], + 'third' => ['priority' => 3, 'visible' => false], + 'fourth' => ['priority' => 2, 'visible' => false], + 'fifth' => ['priority' => 1, 'visible' => false], + ], + ])); + + $columns = array_keys($dataTable->getExportableColumns()); + + $this->assertEquals(['third', 'fourth', 'first'], $columns); + } + + private function createDataTableBuilder(array $options = []): DataTableBuilderInterface + { + return $this->dataTableFactory->createBuilder(DataTableType::class, [], $options); + } +}