Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve action column (extract url creating, move default values to renderer) #142

Merged
merged 7 commits into from
Dec 2, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config/di.php
Original file line number Diff line number Diff line change
@@ -2,15 +2,23 @@

declare(strict_types=1);

use Yiisoft\Definitions\Reference;
use Yiisoft\Translator\CategorySource;
use Yiisoft\Translator\IdMessageReader;
use Yiisoft\Translator\IntlMessageFormatter;
use Yiisoft\Translator\Message\Php\MessageSource;
use Yiisoft\Translator\SimpleMessageFormatter;
use Yiisoft\Yii\DataView\Column\ActionColumnRenderer;
use Yiisoft\Yii\DataView\YiiRouter\ActionColumnUrlCreator;

/** @var array $params */

return [
ActionColumnRenderer::class => [
'__construct()' => [
'defaultUrlCreator' => Reference::to(ActionColumnUrlCreator::class),
],
],
'yii.dataview.categorySource' => [
'definition' => static function () use ($params): CategorySource {
$reader = class_exists(MessageSource::class)
36 changes: 27 additions & 9 deletions src/Column/ActionColumn.php
Original file line number Diff line number Diff line change
@@ -5,36 +5,54 @@
namespace Yiisoft\Yii\DataView\Column;

use Closure;
use Yiisoft\Yii\DataView\Column\Base\DataContext;

/**
* `ActionColumn` is a column for the {@see GridView} widget that displays buttons for viewing and manipulating
* the items.
*
* @psalm-type UrlCreator = callable(string,DataContext):string
*/
final class ActionColumn implements ColumnInterface
{
/**
* @var UrlCreator|null
*/
private $urlCreator;

/**
* @param ?string $primaryKey The primary key of the data to be used to generate an URL automatically.
* @param ?callable $urlCreator A callback that creates a button URL using the specified data information.
*
* @psalm-param UrlCreator|null $urlCreator
* @psalm-param array<string,Closure> $buttons
* @psalm-param array<string,bool|Closure> $visibleButtons
* @psalm-param array<string,bool|Closure>|null $visibleButtons
*/
public function __construct(
public readonly string $primaryKey = 'id',
public readonly string $template = "{view}\n{update}\n{delete}",
public readonly ?string $routeName = null,
public readonly array $urlParamsConfig = [],
public readonly ?array $urlArguments = null,
public readonly array $urlQueryParameters = [],
public readonly ?Closure $urlCreator = null,
public readonly ?string $primaryKey = null,
public readonly ?string $template = null,
public readonly mixed $urlConfig = null,
?callable $urlCreator = null,
public readonly ?string $header = null,
public readonly ?string $footer = null,
public readonly mixed $content = null,
public readonly array $buttons = [],
public readonly array $visibleButtons = [],
public readonly ?array $visibleButtons = null,
public readonly array $columnAttributes = [],
public readonly array $headerAttributes = [],
public readonly array $bodyAttributes = [],
public readonly array $footerAttributes = [],
private readonly bool $visible = true,
) {
$this->urlCreator = $urlCreator;
}

/**
* @psalm-return UrlCreator|null
*/
public function getUrlCreator(): ?callable
{
return $this->urlCreator;
}

public function isVisible(): bool
146 changes: 66 additions & 80 deletions src/Column/ActionColumnRenderer.php
Original file line number Diff line number Diff line change
@@ -6,21 +6,70 @@

use Closure;
use InvalidArgumentException;
use LogicException;
use Yiisoft\Html\Html;
use Yiisoft\Router\CurrentRoute;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Yii\DataView\Column\Base\Cell;
use Yiisoft\Yii\DataView\Column\Base\GlobalContext;
use Yiisoft\Yii\DataView\Column\Base\DataContext;

use function is_object;

/**
* @psalm-import-type UrlCreator from ActionColumn
*/
final class ActionColumnRenderer implements ColumnRendererInterface
{
/**
* @var UrlCreator|null
*/
private $defaultUrlCreator;

/**
* @psalm-var array<string,Closure>
*/
private readonly array $defaultButtons;

/**
* @psalm-param UrlCreator|null $defaultUrlCreator
* @psalm-param array<string,Closure> $defaultButtons
*/
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly CurrentRoute $currentRoute,
?callable $defaultUrlCreator = null,
private readonly string $defaultTemplate = "{view}\n{update}\n{delete}",
?array $defaultButtons = null,
) {
$this->defaultUrlCreator = $defaultUrlCreator;

$this->defaultButtons = $defaultButtons ?? [
'view' => static fn(string $url): string => Html::a(
Html::span('🔎'),
$url,
[
'name' => 'view',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'View',
],
)->render(),
'update' => static fn(string $url): string => Html::a(
Html::span(''),
$url,
[
'name' => 'update',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'Update',
],
)->render(),
'delete' => static fn(string $url): string => Html::a(
Html::span(''),
$url,
[
'name' => 'delete',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'Delete',
],
)->render(),
];
}

public function renderColumn(ColumnInterface $column, Cell $cell, GlobalContext $context): Cell
@@ -51,7 +100,7 @@ public function renderBody(ColumnInterface $column, Cell $cell, DataContext $con
if ($contentSource !== null) {
$content = (string)(is_callable($contentSource) ? $contentSource($context->data, $context) : $contentSource);
} else {
$buttons = empty($column->buttons) ? $this->getDefaultButtons() : $column->buttons;
$buttons = empty($column->buttons) ? $this->defaultButtons : $column->buttons;
$content = preg_replace_callback(
'/{([\w\-\/]+)}/',
function (array $matches) use ($column, $buttons, $context): string {
@@ -67,13 +116,13 @@ function (array $matches) use ($column, $buttons, $context): string {
) &&
isset($buttons[$name])
) {
$url = $this->createUrl($column, $name, $context->data, $context->key);
$url = $this->createUrl($name, $context);
return (string)$buttons[$name]($url);
}

return '';
},
$column->template
$column->template ?? $this->defaultTemplate
);
$content = trim($content);
}
@@ -95,40 +144,17 @@ public function renderFooter(ColumnInterface $column, Cell $cell, GlobalContext
return $cell->addAttributes($column->footerAttributes);
}

private function createUrl(ActionColumn $column, string $action, array|object $data, mixed $key): string
private function createUrl(string $action, DataContext $context): string
{
if ($column->urlCreator !== null) {
return (string) ($column->urlCreator)($action, $data, $key);
}

$primaryKey = $column->primaryKey;
$routeName = $column->routeName;
/** @var ActionColumn $column */
$column = $context->column;

if ($primaryKey !== '') {
$key = (is_object($data) ? $data->$primaryKey : $data[$primaryKey]) ?? $key;
$urlCreator = $column->getUrlCreator() ?? $this->defaultUrlCreator;
if ($urlCreator === null) {
throw new LogicException('Do not set URL creator.');
}

$currentRouteName = $this->currentRoute->getName() ?? '';

$route = $routeName === null
? $currentRouteName . '/' . $action
: $routeName . '/' . $action;

$urlParamsConfig = array_merge(
$column->urlParamsConfig,
is_array($key) ? $key : [$primaryKey => $key]
);

if ($column->urlArguments !== null) {
/** @psalm-var array<string,string> */
$urlArguments = array_merge($column->urlArguments, $urlParamsConfig);
$urlQueryParameters = [];
} else {
$urlArguments = [];
$urlQueryParameters = array_merge($column->urlQueryParameters, $urlParamsConfig);
}

return $this->urlGenerator->generate($route, $urlArguments, $urlQueryParameters);
return $urlCreator($action, $context);
}

private function isVisibleButton(
@@ -140,7 +166,7 @@ private function isVisibleButton(
): bool {
$visibleButtons = $column->visibleButtons;

if (empty($visibleButtons)) {
if ($visibleButtons === null) {
return true;
}

@@ -153,46 +179,6 @@ private function isVisibleButton(
return $visibleValue($data, $key, $index);
}

/**
* Initializes the default button rendering callback for single button.
* @psalm-return array<string,Closure>
*/
private function getDefaultButtons(): array
{
return [
'view' => static fn(string $url): string => Html::a(
Html::span('🔎'),
$url,
[
'name' => 'view',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'View',
],
)->render(),
'update' => static fn(string $url): string => Html::a(
Html::span(''),
$url,
[
'name' => 'update',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'Update',
],
)->render(),
'delete' => static fn(string $url): string => Html::a(
Html::span(''),
$url,
[
'name' => 'delete',
'role' => 'button',
'style' => 'text-decoration: none!important;',
'title' => 'Delete',
],
)->render(),
];
}

/**
* @psalm-assert ActionColumn $column
*/
2 changes: 1 addition & 1 deletion src/Column/Base/DataContext.php
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ final class DataContext
public function __construct(
public readonly ColumnInterface $column,
public readonly array|object $data,
public readonly mixed $key,
public readonly int|string $key,
public readonly int $index,
) {
}
5 changes: 1 addition & 4 deletions src/Column/CheckboxColumnRenderer.php
Original file line number Diff line number Diff line change
@@ -54,10 +54,7 @@ public function renderBody(ColumnInterface $column, Cell $cell, DataContext $con
}

if (!array_key_exists('value', $inputAttributes)) {
$key = $context->key;
$value = is_array($key)
? json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
: (string)$key;
$value = $context->key;
}

$input = Html::checkbox($name, $value, $inputAttributes);
5 changes: 1 addition & 4 deletions src/Column/RadioColumnRenderer.php
Original file line number Diff line number Diff line change
@@ -51,10 +51,7 @@ public function renderBody(ColumnInterface $column, Cell $cell, DataContext $con
}

if (!array_key_exists('value', $inputAttributes)) {
$key = $context->key;
$value = is_array($key)
? json_encode($key, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
: (string)$key;
$value = $context->key;
}

$input = Html::radio($name, $value, $inputAttributes);
55 changes: 55 additions & 0 deletions src/YiiRouter/ActionColumnUrlCreator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\DataView\YiiRouter;

use LogicException;
use Yiisoft\Router\CurrentRoute;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Yii\DataView\Column\ActionColumn;
use Yiisoft\Yii\DataView\Column\Base\DataContext;

use function is_object;

final class ActionColumnUrlCreator
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly CurrentRoute $currentRoute,
private readonly string $defaultPrimaryKey = 'id',
private readonly bool $defaultPrimaryKeyPlace = UrlConfig::ARGUMENTS,
) {
}

public function __invoke(string $action, DataContext $context): string
{
/** @var ActionColumn $column */
$column = $context->column;

$config = $column->urlConfig ?? new UrlConfig();
if (!$config instanceof UrlConfig) {
throw new LogicException(self::class . ' supports ' . UrlConfig::class . ' only.');
}

$primaryKey = $column->primaryKey ?? $this->defaultPrimaryKey;
$primaryKeyPlace = $config->primaryKeyPlace ?? $this->defaultPrimaryKeyPlace;

$primaryKeyValue = is_object($context->data)
? $context->data->$primaryKey
: $context->data[$primaryKey];

/** @psalm-suppress PossiblyNullOperand We guess that current route is matched. */
$route = ($config->baseRouteName ?? $this->currentRoute->getName()) . '/' . $action;

$arguments = $config->arguments;
$queryParameters = $config->queryParameters;
if ($primaryKeyPlace === UrlConfig::ARGUMENTS) {
$arguments = array_merge($arguments, [$primaryKey => (string)$primaryKeyValue]);
} else {
$queryParameters = array_merge($queryParameters, [$primaryKey => (string)$primaryKeyValue]);
}

return $this->urlGenerator->generate($route, $arguments, $queryParameters);
}
}
24 changes: 24 additions & 0 deletions src/YiiRouter/UrlConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Yii\DataView\YiiRouter;

use Stringable;

final class UrlConfig
{
public const ARGUMENTS = true;
public const QUERY_PARAMETERS = false;

/**
* @psalm-param array<string,scalar|Stringable|null> $arguments
*/
public function __construct(
public readonly ?string $baseRouteName = null,
public readonly array $arguments = [],
public readonly array $queryParameters = [],
public readonly ?bool $primaryKeyPlace = null,
) {
}
}
56 changes: 13 additions & 43 deletions tests/Column/ActionColumnTest.php
Original file line number Diff line number Diff line change
@@ -12,10 +12,12 @@
use Yiisoft\Factory\NotFoundException;
use Yiisoft\Html\Html;
use Yiisoft\Yii\DataView\Column\ActionColumn;
use Yiisoft\Yii\DataView\Column\Base\DataContext;
use Yiisoft\Yii\DataView\Column\DataColumn;
use Yiisoft\Yii\DataView\GridView;
use Yiisoft\Yii\DataView\Tests\Support\Assert;
use Yiisoft\Yii\DataView\Tests\Support\TestTrait;
use Yiisoft\Yii\DataView\YiiRouter\UrlConfig;

final class ActionColumnTest extends TestCase
{
@@ -536,7 +538,7 @@ public function testUrlArguments(): void
</div>
HTML,
GridView::widget()
->columns(new ActionColumn(urlArguments: ['test-arguments' => 'test.arguments']))
->columns(new ActionColumn(urlConfig: new UrlConfig(arguments: ['test-arguments' => 'test.arguments'])))
->id('w1-grid')
->dataReader($this->createOffsetPaginator($this->data, 10))
->render()
@@ -585,8 +587,8 @@ public function testUrlCreator(): void
new ActionColumn(
urlCreator: static fn(
string $action,
array $data
): string => 'https://test.com/' . $action . '?id=' . $data['id'],
DataContext $context
): string => 'https://test.com/' . $action . '?id=' . $context->data['id'],
)
)
->id('w1-grid')
@@ -627,46 +629,14 @@ public function testUrlQueryParameters(): void
</div>
HTML,
GridView::widget()
->columns(new ActionColumn(urlQueryParameters: ['test-param' => 'test.param']))
->id('w1-grid')
->dataReader($this->createOffsetPaginator($this->data, 10))
->render()
);
}

public function testUrlParamsConfig(): void
{
Assert::equalsWithoutLE(
<<<HTML
<div id="w1-grid">
<table>
<thead>
<tr>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a name="view" href="/admin/manage/view?test-param=test.param&amp;id=1" title="View" role="button" style="text-decoration: none!important;"><span>🔎</span></a>
<a name="update" href="/admin/manage/update?test-param=test.param&amp;id=1" title="Update" role="button" style="text-decoration: none!important;"><span>✎</span></a>
<a name="delete" href="/admin/manage/delete?test-param=test.param&amp;id=1" title="Delete" role="button" style="text-decoration: none!important;"><span>❌</span></a>
</td>
</tr>
<tr>
<td>
<a name="view" href="/admin/manage/view?test-param=test.param&amp;id=2" title="View" role="button" style="text-decoration: none!important;"><span>🔎</span></a>
<a name="update" href="/admin/manage/update?test-param=test.param&amp;id=2" title="Update" role="button" style="text-decoration: none!important;"><span>✎</span></a>
<a name="delete" href="/admin/manage/delete?test-param=test.param&amp;id=2" title="Delete" role="button" style="text-decoration: none!important;"><span>❌</span></a>
</td>
</tr>
</tbody>
</table>
<div>Page <b>1</b> of <b>1</b></div>
</div>
HTML,
GridView::widget()
->columns(new ActionColumn(urlParamsConfig: ['test-param' => 'test.param']))
->columns(
new ActionColumn(
urlConfig: new UrlConfig(
queryParameters: ['test-param' => 'test.param'],
primaryKeyPlace: UrlConfig::QUERY_PARAMETERS,
),
),
)
->id('w1-grid')
->dataReader($this->createOffsetPaginator($this->data, 10))
->render()
8 changes: 8 additions & 0 deletions tests/Support/TestTrait.php
Original file line number Diff line number Diff line change
@@ -9,12 +9,15 @@
use Yiisoft\Data\Reader\Iterable\IterableDataReader;
use Yiisoft\Data\Reader\Sort;
use Yiisoft\Definitions\Exception\InvalidConfigException;
use Yiisoft\Definitions\Reference;
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;
use Yiisoft\Router\CurrentRoute;
use Yiisoft\Router\Route;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Widget\WidgetFactory;
use Yiisoft\Yii\DataView\Column\ActionColumnRenderer;
use Yiisoft\Yii\DataView\YiiRouter\ActionColumnUrlCreator;

trait TestTrait
{
@@ -64,6 +67,11 @@ private function config(): array
return [
CurrentRoute::class => $currentRoute,
UrlGeneratorInterface::class => Mock::urlGenerator([], $currentRoute),
ActionColumnRenderer::class => [
'__construct()' => [
'defaultUrlCreator' => Reference::to(ActionColumnUrlCreator::class),
],
],
];
}
}