diff --git a/composer.json b/composer.json index 5e14842df..917749513 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,7 @@ "yiisoft/router": "^3.0", "yiisoft/strings": "^2.0", "yiisoft/translator": "^3.0", + "yiisoft/validator": "^1.1", "yiisoft/view": "^8.0", "yiisoft/widget": "^2.0" }, diff --git a/config/widgets-themes.php b/config/widgets-themes.php index f69970b3e..2240b67e1 100644 --- a/config/widgets-themes.php +++ b/config/widgets-themes.php @@ -3,6 +3,8 @@ declare(strict_types=1); use Yiisoft\Yii\DataView\Column\ActionColumnRenderer; +use Yiisoft\Yii\DataView\Filter\Widget\DropdownFilter; +use Yiisoft\Yii\DataView\Filter\Widget\TextInputFilter; use Yiisoft\Yii\DataView\GridView; use Yiisoft\Yii\DataView\KeysetPagination; use Yiisoft\Yii\DataView\OffsetPagination; @@ -17,6 +19,9 @@ 'sortableHeaderPrepend()' => ['
'], 'sortableHeaderAscPrepend()' => ['
'], 'sortableHeaderDescPrepend()' => ['
'], + 'filterCellAttributes()' => [['class' => 'align-top']], + 'filterCellInvalidClass()' => ['bg-danger bg-opacity-10'], + 'filterErrorsContainerAttributes()' => [['class' => 'text-danger mt-1']], 'addColumnRendererConfigs()' => [ [ ActionColumnRenderer::class => [ @@ -25,6 +30,12 @@ ], ], ], + DropdownFilter::class => [ + 'attributes()' => [['class' => 'form-select']], + ], + TextInputFilter::class => [ + 'attributes()' => [['class' => 'form-control']], + ], OffsetPagination::class => [ 'listTag()' => ['ul'], 'listAttributes()' => [['class' => 'pagination']], diff --git a/src/BaseListView.php b/src/BaseListView.php index 7ec16e2b6..6d16d581b 100644 --- a/src/BaseListView.php +++ b/src/BaseListView.php @@ -12,7 +12,9 @@ use Yiisoft\Data\Paginator\PageToken; use Yiisoft\Data\Paginator\PaginatorInterface; use Yiisoft\Data\Reader\CountableDataInterface; +use Yiisoft\Data\Reader\Filter\All; use Yiisoft\Data\Reader\FilterableDataInterface; +use Yiisoft\Data\Reader\FilterInterface; use Yiisoft\Data\Reader\LimitableDataInterface; use Yiisoft\Data\Reader\OffsetableDataInterface; use Yiisoft\Data\Reader\ReadableDataInterface; @@ -26,6 +28,7 @@ use Yiisoft\Translator\SimpleMessageFormatter; use Yiisoft\Translator\Translator; use Yiisoft\Translator\TranslatorInterface; +use Yiisoft\Validator\Result as ValidationResult; use Yiisoft\Widget\Widget; use Yiisoft\Yii\DataView\Exception\DataReaderNotSetException; @@ -83,7 +86,7 @@ abstract class BaseListView extends Widget protected array $urlArguments = []; protected array $urlQueryParameters = []; - private UrlParameterProviderInterface|null $urlParameterProvider = null; + protected UrlParameterProviderInterface|null $urlParameterProvider = null; private bool $ignoreMissingPage = true; @@ -170,11 +173,11 @@ final public function urlParameterProvider(?UrlParameterProviderInterface $provi /** * Renders the data models. * - * @return string the rendering result. + * @return string The rendering result. * * @psalm-param array $items */ - abstract protected function renderItems(array $items): string; + abstract protected function renderItems(array $items, ValidationResult $filterValidationResult): string; final public function containerTag(?string $tag): static { @@ -240,11 +243,21 @@ public function getDataReader(): ReadableDataInterface } /** + * @psalm-return list{FilterInterface[],ValidationResult} + */ + protected function makeFilters(): array + { + return [[], new ValidationResult()]; + } + + /** + * @param FilterInterface[] $filters + * * @throws PageNotFoundException * * @psalm-return array */ - private function prepareDataReaderAndGetItems(): array + private function prepareDataReaderAndGetItems(array $filters): array { $page = $this->urlParameterProvider?->get( $this->urlConfig->getPageParameterName(), @@ -263,7 +276,7 @@ private function prepareDataReaderAndGetItems(): array $this->urlConfig->getSortParameterType(), ); - $this->preparedDataReader = $this->prepareDataReaderByParams($page, $previousPage, $pageSize, $sort); + $this->preparedDataReader = $this->prepareDataReaderByParams($page, $previousPage, $pageSize, $sort, $filters); try { return $this->getItems($this->preparedDataReader); @@ -271,7 +284,7 @@ private function prepareDataReaderAndGetItems(): array } if ($this->ignoreMissingPage) { - $this->preparedDataReader = $this->prepareDataReaderByParams(null, null, $pageSize, $sort); + $this->preparedDataReader = $this->prepareDataReaderByParams(null, null, $pageSize, $sort, $filters); try { return $this->getItems($this->preparedDataReader); } catch (PageNotFoundException $exception) { @@ -296,11 +309,15 @@ private function getItems(ReadableDataInterface $dataReader): array return is_array($items) ? $items : iterator_to_array($items); } + /** + * @param FilterInterface[] $filters + */ private function prepareDataReaderByParams( ?string $page, ?string $previousPage, ?string $pageSize, ?string $sort, + array $filters, ): ReadableDataInterface { $dataReader = $this->getDataReader(); @@ -345,6 +362,10 @@ private function prepareDataReaderByParams( } } + if ($dataReader->isFilterable() && !empty($filters)) { + $dataReader = $dataReader->withFilter(new All(...$filters)); + } + return $dataReader; } @@ -528,7 +549,8 @@ protected function renderEmpty(int $colspan): Td public function render(): string { - $items = $this->prepareDataReaderAndGetItems(); + [$filters, $filterValidationResult] = $this->makeFilters(); + $items = $this->prepareDataReaderAndGetItems($filters); $content = trim( strtr( @@ -536,7 +558,7 @@ public function render(): string [ '{header}' => $this->renderHeader(), '{toolbar}' => $this->toolbar, - '{items}' => $this->renderItems($items), + '{items}' => $this->renderItems($items, $filterValidationResult), '{summary}' => $this->renderSummary(), '{pager}' => $this->renderPagination(), ], diff --git a/src/Column/Base/Cell.php b/src/Column/Base/Cell.php index acbf025bc..2a3e0ed0c 100644 --- a/src/Column/Base/Cell.php +++ b/src/Column/Base/Cell.php @@ -12,11 +12,17 @@ final class Cell { private bool $doubleEncode = true; + /** + * @psalm-var array + */ + private array $content; + public function __construct( private array $attributes = [], private ?bool $encode = null, - private string|Stringable $content = '', + string|Stringable ...$content, ) { + $this->content = $content; } /** @@ -48,7 +54,7 @@ public function doubleEncode(bool $doubleEncode): self /** * @param string|Stringable $content Tag content. */ - public function content(string|Stringable $content): self + public function content(string|Stringable ...$content): self { $new = clone $this; $new->content = $content; @@ -115,8 +121,21 @@ public function isDoubleEncode(): bool return $this->doubleEncode; } - public function getContent(): string|Stringable + /** + * @psalm-return array + */ + public function getContent(): array { return $this->content; } + + public function isEmptyContent(): bool + { + foreach ($this->content as $content) { + if (!empty((string) $content)) { + return false; + } + } + return true; + } } diff --git a/src/Column/Base/FilterContext.php b/src/Column/Base/FilterContext.php new file mode 100644 index 000000000..19ee50d9a --- /dev/null +++ b/src/Column/Base/FilterContext.php @@ -0,0 +1,26 @@ +urlParameterProvider?->get($name, UrlParameterType::QUERY); + } +} diff --git a/src/Column/Base/MakeFilterContext.php b/src/Column/Base/MakeFilterContext.php new file mode 100644 index 000000000..a518bcb82 --- /dev/null +++ b/src/Column/Base/MakeFilterContext.php @@ -0,0 +1,23 @@ +urlParameterProvider?->get($name, UrlParameterType::QUERY); + } +} diff --git a/src/Column/DataColumn.php b/src/Column/DataColumn.php index e19f06a66..bee0dd8b2 100644 --- a/src/Column/DataColumn.php +++ b/src/Column/DataColumn.php @@ -4,6 +4,10 @@ namespace Yiisoft\Yii\DataView\Column; +use Yiisoft\Validator\RuleInterface; +use Yiisoft\Yii\DataView\Filter\Factory\FilterFactoryInterface; +use Yiisoft\Yii\DataView\Filter\Widget\FilterWidget; + /** * DetailColumn is the default column type for the {@see GridView} widget. * @@ -20,6 +24,10 @@ final class DataColumn implements ColumnInterface { public readonly ?string $queryProperty; + /** + * @psalm-param bool|array>|FilterWidget $filter + * @psalm-param RuleInterface[]|RuleInterface|null $filterValidation + */ public function __construct( public readonly ?string $property = null, ?string $queryProperty = null, @@ -32,6 +40,9 @@ public function __construct( public readonly bool $withSorting = true, public readonly mixed $content = null, public readonly ?string $dateTimeFormat = null, + public readonly bool|array|FilterWidget $filter = false, + public readonly string|FilterFactoryInterface|null $filterFactory = null, + public readonly array|RuleInterface|null $filterValidation = null, private readonly bool $visible = true, ) { $this->queryProperty = $queryProperty ?? $this->property; diff --git a/src/Column/DataColumnRenderer.php b/src/Column/DataColumnRenderer.php index fbc16b94e..7e06a9308 100644 --- a/src/Column/DataColumnRenderer.php +++ b/src/Column/DataColumnRenderer.php @@ -6,17 +6,32 @@ use DateTimeInterface; use InvalidArgumentException; +use Psr\Container\ContainerInterface; use Yiisoft\Arrays\ArrayHelper; +use Yiisoft\Data\Reader\FilterInterface; use Yiisoft\Html\Html; +use Yiisoft\Validator\ValidatorInterface; use Yiisoft\Yii\DataView\Column\Base\Cell; use Yiisoft\Yii\DataView\Column\Base\DataContext; +use Yiisoft\Yii\DataView\Column\Base\FilterContext; use Yiisoft\Yii\DataView\Column\Base\GlobalContext; use Yiisoft\Yii\DataView\Column\Base\HeaderContext; - -final class DataColumnRenderer implements ColumnRendererInterface +use Yiisoft\Yii\DataView\Column\Base\MakeFilterContext; +use Yiisoft\Yii\DataView\Filter\Factory\EqualsFilterFactory; +use Yiisoft\Yii\DataView\Filter\Factory\FilterFactoryInterface; +use Yiisoft\Yii\DataView\Filter\Factory\LikeFilterFactory; +use Yiisoft\Yii\DataView\Filter\Widget\Context; +use Yiisoft\Yii\DataView\Filter\Widget\DropdownFilter; +use Yiisoft\Yii\DataView\Filter\Widget\TextInputFilter; + +final class DataColumnRenderer implements FilterableColumnRendererInterface { public function __construct( + private readonly ContainerInterface $filterFactoryContainer, + private readonly ValidatorInterface $validator, private readonly string $dateTimeFormat = 'Y-m-d H:i:s', + private readonly string $defaultFilterFactory = LikeFilterFactory::class, + private readonly string $defaultArrayFilterFactory = EqualsFilterFactory::class, ) { } @@ -53,6 +68,82 @@ public function renderHeader(ColumnInterface $column, Cell $cell, HeaderContext return $cell->content($prepend . ($link ?? $label) . $append); } + public function renderFilter(ColumnInterface $column, Cell $cell, FilterContext $context): ?Cell + { + $this->checkColumn($column); + + if ($column->queryProperty === null || $column->filter === false) { + return null; + } + + if ($column->filter === true) { + $widget = TextInputFilter::widget(); + } elseif (is_array($column->filter)) { + $widget = DropdownFilter::widget()->optionsData($column->filter); + } else { + $widget = $column->filter; + } + + $content = [ + $widget->withContext( + new Context( + $column->queryProperty, + $context->getQueryValue($column->queryProperty), + $context->formId + ) + ), + ]; + + $errors = $context->validationResult->getAttributeErrorMessages($column->queryProperty); + if (!empty($errors)) { + $cell = $cell->addClass($context->cellInvalidClass); + $content[] = Html::div(attributes: $context->errorsContainerAttributes) + ->content(...array_map(static fn(string $error) => Html::div($error), $errors)); + } + + return $cell->content(...$content)->encode(false); + } + + public function makeFilter(ColumnInterface $column, MakeFilterContext $context): ?FilterInterface + { + $this->checkColumn($column); + if ($column->queryProperty === null) { + return null; + } + + $value = $context->getQueryValue($column->queryProperty); + if ($column->filterValidation !== null) { + $result = $this->validator->validate($value, $column->filterValidation); + if (!$result->isValid()) { + foreach ($result->getErrors() as $error) { + $context->validationResult->addError( + $error->getMessage(), + $error->getParameters(), + [$column->queryProperty] + ); + } + return null; + } + } + if ($value === null || $value === '') { + return null; + } + + if ($column->filterFactory === null) { + /** @var FilterFactoryInterface $factory */ + $factory = $this->filterFactoryContainer->get( + is_array($column->filter) ? $this->defaultArrayFilterFactory : $this->defaultFilterFactory + ); + } elseif (is_string($column->filterFactory)) { + /** @var FilterFactoryInterface $factory */ + $factory = $this->filterFactoryContainer->get($column->filterFactory); + } else { + $factory = $column->filterFactory; + } + + return $factory->create($column->queryProperty, $value); + } + public function renderBody(ColumnInterface $column, Cell $cell, DataContext $context): Cell { $this->checkColumn($column); diff --git a/src/Column/FilterableColumnRendererInterface.php b/src/Column/FilterableColumnRendererInterface.php new file mode 100644 index 000000000..aded5537e --- /dev/null +++ b/src/Column/FilterableColumnRendererInterface.php @@ -0,0 +1,17 @@ +> $data + */ + public function optionsData( + array $data, + bool $encode = true, + array $optionsAttributes = [], + array $groupsAttributes = [] + ): self { + $new = clone $this; + $new->select = $this->getSelect()->optionsData($data, $encode, $optionsAttributes, $groupsAttributes); + return $new; + } + + /** + * Add a set of attributes to existing tag attributes. + * Same named attributes are replaced. + * + * @param array $attributes Name-value set of attributes. + * + * @see Select::addAttributes() + */ + public function addAttributes(array $attributes): static + { + $new = clone $this; + $new->select = $this->getSelect()->addAttributes($attributes); + return $new; + } + + /** + * Replace attributes with a new set. + * + * @param array $attributes Name-value set of attributes. + * + * @see Select::attributes() + */ + public function attributes(array $attributes): self + { + $new = clone $this; + $new->select = $this->getSelect()->attributes($attributes); + return $new; + } + + public function renderFilter(Context $context): string + { + $select = $this->getSelect() + ->name($context->property) + ->form($context->formId) + ->attribute('onChange', 'this.form.submit()'); + + if ($context->value !== null) { + $select = $select->value($context->value); + } + + return $select->render(); + } + + private function getSelect(): Select + { + return $this->select ?? Select::tag()->prompt(''); + } +} diff --git a/src/Filter/Widget/FilterWidget.php b/src/Filter/Widget/FilterWidget.php new file mode 100644 index 000000000..f271a5e1e --- /dev/null +++ b/src/Filter/Widget/FilterWidget.php @@ -0,0 +1,26 @@ +context = $context; + return $new; + } + + final public function render(): string + { + return $this->renderFilter($this->context); + } + + abstract public function renderFilter(Context $context): string; +} diff --git a/src/Filter/Widget/TextInputFilter.php b/src/Filter/Widget/TextInputFilter.php new file mode 100644 index 000000000..936368b5a --- /dev/null +++ b/src/Filter/Widget/TextInputFilter.php @@ -0,0 +1,56 @@ +input = $this->getInput()->addAttributes($attributes); + return $new; + } + + /** + * Replace attributes with a new set. + * + * @param array $attributes Name-value set of attributes. + * + * @see Input::attributes() + */ + public function attributes(array $attributes): self + { + $new = clone $this; + $new->input = $this->getInput()->attributes($attributes); + return $new; + } + + public function renderFilter(Context $context): string + { + return $this->getInput() + ->name($context->property) + ->value($context->value) + ->form($context->formId) + ->render(); + } + + private function getInput(): Input + { + return $this->input ?? Html::textInput(); + } +} diff --git a/src/GridView.php b/src/GridView.php index a420911bf..dfcd79065 100644 --- a/src/GridView.php +++ b/src/GridView.php @@ -14,13 +14,17 @@ use Yiisoft\Html\Html; use Yiisoft\Html\Tag\Tr; use Yiisoft\Translator\TranslatorInterface; -use Yiisoft\Yii\DataView\Column\ActionColumn; +use Yiisoft\Validator\Result as ValidationResult; use Yiisoft\Yii\DataView\Column\Base\Cell; +use Yiisoft\Yii\DataView\Column\Base\FilterContext; use Yiisoft\Yii\DataView\Column\Base\GlobalContext; use Yiisoft\Yii\DataView\Column\Base\DataContext; use Yiisoft\Yii\DataView\Column\Base\HeaderContext; +use Yiisoft\Yii\DataView\Column\Base\MakeFilterContext; use Yiisoft\Yii\DataView\Column\Base\RendererContainer; use Yiisoft\Yii\DataView\Column\ColumnInterface; +use Yiisoft\Yii\DataView\Column\ColumnRendererInterface; +use Yiisoft\Yii\DataView\Column\FilterableColumnRendererInterface; /** * The GridView widget is used to display data in a grid. @@ -42,6 +46,11 @@ final class GridView extends BaseListView */ private array $columns = []; + /** + * @var ColumnInterface[] + */ + private ?array $columnsCache = null; + private bool $columnsGroupEnabled = false; private string $emptyCell = ' '; private bool $footerEnabled = false; @@ -69,8 +78,17 @@ final class GridView extends BaseListView private ?string $sortableLinkAscClass = null; private ?string $sortableLinkDescClass = null; + private array $filterCellAttributes = []; + private ?string $filterCellInvalidClass = null; + private array $filterErrorsContainerAttributes = []; + private RendererContainer $columnRendererContainer; + /** + * @var ColumnRendererInterface[] + */ + private ?array $columnRenderersCache = null; + public function __construct( ContainerInterface $columnRenderersDependencyContainer, TranslatorInterface|null $translator = null, @@ -89,6 +107,32 @@ public function addColumnRendererConfigs(array $configs): self return $new; } + /** + * Return new instance with the HTML attributes for the filter cell (`td`) tag. + * + * @param array $attributes The tag attributes in terms of name-value pairs. + */ + public function filterCellAttributes(array $attributes): self + { + $new = clone $this; + $new->filterCellAttributes = $attributes; + return $new; + } + + public function filterCellInvalidClass(?string $class): self + { + $new = clone $this; + $new->filterCellInvalidClass = $class; + return $new; + } + + public function filterErrorsContainerAttributes(array $attributes): self + { + $new = clone $this; + $new->filterErrorsContainerAttributes = $attributes; + return $new; + } + public function enableMultiSort(bool $value = true): self { $new = clone $this; @@ -397,19 +441,13 @@ public function sortableHeaderDescAppend(string|Stringable $content): self /** * Renders the data active record classes for the grid view. */ - protected function renderItems(array $items): string + protected function renderItems(array $items, ValidationResult $filterValidationResult): string { - $columns = array_filter( - $this->columns, - static fn(ColumnInterface $column) => $column->isVisible() - ); - - $renderers = []; - foreach ($columns as $i => $column) { - $renderers[$i] = $this->columnRendererContainer->get($column->getRenderer()); - } + $columns = $this->getColumns(); + $renderers = $this->getColumnRenderers(); $blocks = []; + $filtersForm = ''; $dataReader = $this->getDataReader(); $globalContext = new GlobalContext( @@ -420,6 +458,69 @@ protected function renderItems(array $items): string $this->translationCategory, ); + if ($this->preparedDataReader instanceof PaginatorInterface) { + $pageToken = $this->preparedDataReader->isOnFirstPage() ? null : $this->preparedDataReader->getToken(); + $pageSize = $this->preparedDataReader->getPageSize(); + if ($pageSize === $this->getDefaultPageSize()) { + $pageSize = null; + } + } else { + $pageToken = null; + $pageSize = null; + } + + $tags = []; + $hasFilters = false; + $filterContext = new FilterContext( + formId: Html::generateId(), + validationResult: $filterValidationResult, + cellInvalidClass: $this->filterCellInvalidClass, + errorsContainerAttributes: $this->filterErrorsContainerAttributes, + urlParameterProvider: $this->urlParameterProvider, + ); + foreach ($columns as $i => $column) { + $cell = $renderers[$i] instanceof FilterableColumnRendererInterface + ? $renderers[$i]->renderFilter($column, new Cell(attributes: $this->filterCellAttributes), $filterContext) + : null; + if ($cell === null) { + $tags[] = Html::td(' ')->encode(false); + } else { + $tags[] = Html::td(attributes: $cell->getAttributes()) + ->content(...$cell->getContent()) + ->encode($cell->isEncode()) + ->doubleEncode($cell->isDoubleEncode()); + $hasFilters = true; + } + } + if ($hasFilters) { + $sort = $this->urlParameterProvider?->get( + $this->urlConfig->getSortParameterName(), + $this->urlConfig->getSortParameterType() + ); + $url = $this->urlCreator === null ? '' : call_user_func_array( + $this->urlCreator, + UrlParametersFactory::create( + null, + $this->urlConfig->getPageSizeParameterType() === UrlParameterType::PATH ? $pageSize : null, + $this->urlConfig->getSortParameterType() === UrlParameterType::PATH ? $sort : null, + $this->urlConfig, + ) + ); + $content = [Html::submitButton()]; + if ($this->urlConfig->getPageSizeParameterType() === UrlParameterType::QUERY && !empty($pageSize)) { + $content[] = Html::hiddenInput($this->urlConfig->getPageSizeParameterName(), $pageSize); + } + if ($this->urlConfig->getSortParameterType() === UrlParameterType::QUERY && !empty($sort)) { + $content[] = Html::hiddenInput($this->urlConfig->getSortParameterName(), $sort); + } + $filtersForm = Html::form($url, 'GET', ['id' => $filterContext->formId, 'style' => 'display:none']) + ->content(...$content) + ->render(); + $filterRow = Html::tr()->cells(...$tags); + } else { + $filterRow = null; + } + if ($this->columnsGroupEnabled) { $tags = []; foreach ($columns as $i => $column) { @@ -430,16 +531,6 @@ protected function renderItems(array $items): string } if ($this->headerTableEnabled) { - if ($this->preparedDataReader instanceof PaginatorInterface) { - $pageToken = $this->preparedDataReader->isOnFirstPage() ? null : $this->preparedDataReader->getToken(); - $pageSize = $this->preparedDataReader->getPageSize(); - if ($pageSize === $this->getDefaultPageSize()) { - $pageSize = null; - } - } else { - $pageToken = null; - $pageSize = null; - } $headerContext = new HeaderContext( $this->getSort($dataReader), $this->getSort($this->preparedDataReader), @@ -469,12 +560,16 @@ protected function renderItems(array $items): string $tags[] = $cell === null ? Html::th(' ')->encode(false) : Html::th(attributes: $cell->getAttributes()) - ->content($cell->getContent()) + ->content(...$cell->getContent()) ->encode($cell->isEncode()) ->doubleEncode($cell->isDoubleEncode()); } $headerRow = Html::tr($this->headerRowAttributes)->cells(...$tags); - $blocks[] = Html::thead()->rows($headerRow)->render(); + $thead = Html::thead()->rows($headerRow); + if ($filterRow !== null) { + $thead = $thead->addRows($filterRow); + } + $blocks[] = $thead->render(); } if ($this->footerEnabled) { @@ -485,10 +580,8 @@ protected function renderItems(array $items): string (new Cell())->content(' ')->encode(false), $globalContext ); - /** @var string|Stringable $content */ - $content = $cell->getContent(); $tags[] = Html::td(attributes: $cell->getAttributes()) - ->content($content) + ->content(...$cell->getContent()) ->encode($cell->isEncode()) ->doubleEncode($cell->isDoubleEncode()); } @@ -511,11 +604,10 @@ protected function renderItems(array $items): string foreach ($columns as $i => $column) { $context = new DataContext($column, $value, $key, $index); $cell = $renderers[$i]->renderBody($column, new Cell(), $context); - $content = $cell->getContent(); - $tags[] = empty($content) + $tags[] = $cell->isEmptyContent() ? Html::td()->content($this->emptyCell)->encode(false) : Html::td(attributes: $this->prepareBodyAttributes($cell->getAttributes(), $context)) - ->content($content) + ->content(...$cell->getContent()) ->encode($cell->isEncode()) ->doubleEncode($cell->isDoubleEncode()); } @@ -537,13 +629,36 @@ protected function renderItems(array $items): string ->render() : Html::tbody($this->tbodyAttributes)->rows(...$rows)->render(); - return Html::tag('table', attributes: $this->tableAttributes)->open() + return + $filtersForm . + Html::tag('table', attributes: $this->tableAttributes)->open() . "\n" . implode("\n", $blocks) . "\n" . ''; } + protected function makeFilters(): array + { + $columns = $this->getColumns(); + $renderers = $this->getColumnRenderers(); + + $validationResult = new ValidationResult(); + $context = new MakeFilterContext($validationResult, $this->urlParameterProvider); + + $filters = []; + foreach ($columns as $i => $column) { + if ($renderers[$i] instanceof FilterableColumnRendererInterface) { + $filter = $renderers[$i]->makeFilter($column, $context); + if ($filter !== null) { + $filters[] = $filter; + } + } + } + + return [$filters, $validationResult]; + } + private function prepareBodyAttributes(array $attributes, DataContext $context): array { foreach ($attributes as $i => $attribute) { @@ -567,4 +682,34 @@ private function getSort(?ReadableDataInterface $dataReader): ?Sort return null; } + + /** + * @return ColumnInterface[] + */ + private function getColumns(): array + { + if ($this->columnsCache === null) { + $this->columnsCache = array_filter( + $this->columns, + static fn(ColumnInterface $column) => $column->isVisible() + ); + } + + return $this->columnsCache; + } + + /** + * @return ColumnRendererInterface[] + */ + private function getColumnRenderers(): array + { + if ($this->columnRenderersCache === null) { + $this->columnRenderersCache = array_map( + fn(ColumnInterface $column) => $this->columnRendererContainer->get($column->getRenderer()), + $this->getColumns() + ); + } + + return $this->columnRenderersCache; + } } diff --git a/src/ListView.php b/src/ListView.php index c17adeed0..af4e483ae 100644 --- a/src/ListView.php +++ b/src/ListView.php @@ -216,7 +216,7 @@ protected function renderItem(array|object $data, mixed $key, int $index): strin * * @throws Throwable|ViewNotFoundException */ - protected function renderItems(array $items): string + protected function renderItems(array $items, \Yiisoft\Validator\Result $filterValidationResult): string { $keys = array_keys($items); $rows = []; diff --git a/tests/GridView/ImmutableTest.php b/tests/GridView/ImmutableTest.php index b3a027428..40eee4bb8 100644 --- a/tests/GridView/ImmutableTest.php +++ b/tests/GridView/ImmutableTest.php @@ -67,7 +67,7 @@ public function testGridView(): void private function createBaseListView(): DataView\BaseListView { return new class () extends DataView\BaseListView { - public function renderItems(array $items): string + public function renderItems(array $items, \Yiisoft\Validator\Result $filterValidationResult): string { return ''; } diff --git a/tests/GridView/TranslatorTest.php b/tests/GridView/TranslatorTest.php index 94f1f6c16..ab540ec17 100644 --- a/tests/GridView/TranslatorTest.php +++ b/tests/GridView/TranslatorTest.php @@ -11,9 +11,10 @@ use Yiisoft\Definitions\Reference; use Yiisoft\Di\Container; use Yiisoft\Di\ContainerConfig; -use Yiisoft\Di\NotFoundException; use Yiisoft\Translator\Translator; use Yiisoft\Translator\TranslatorInterface; +use Yiisoft\Validator\Validator; +use Yiisoft\Validator\ValidatorInterface; use Yiisoft\Widget\WidgetFactory; use Yiisoft\Yii\DataView\Column\DataColumn; use Yiisoft\Yii\DataView\Column\SerialColumn; @@ -33,12 +34,6 @@ final class TranslatorTest extends TestCase ]; private TranslatorInterface $translator; - /** - * @throws CircularReferenceException - * @throws InvalidConfigException - * @throws NotInstantiableException - * @throws NotFoundException - */ protected function setUp(): void { $container = new Container(ContainerConfig::create()->withDefinitions($this->config())); @@ -329,6 +324,7 @@ private function config(): array 'categories' => Reference::to('tag@translation.categorySource'), ], ], + ValidatorInterface::class => Validator::class, ], $containerDefinitions, ); diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index 5c66f4b1b..7abdb1208 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -16,6 +16,8 @@ use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Route; use Yiisoft\Router\UrlGeneratorInterface; +use Yiisoft\Validator\Validator; +use Yiisoft\Validator\ValidatorInterface; use Yiisoft\Widget\WidgetFactory; use Yiisoft\Yii\DataView\Column\ActionColumnRenderer; use Yiisoft\Yii\DataView\GridView; @@ -73,6 +75,7 @@ private function config(): array return [ CurrentRoute::class => $currentRoute, UrlGeneratorInterface::class => Mock::urlGenerator([], $currentRoute), + ValidatorInterface::class => Validator::class, ]; } }