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

[Feature Request] link() helper on CrudColumn #5317

Merged
merged 14 commits into from
Oct 31, 2023
1 change: 1 addition & 0 deletions src/app/Library/CrudPanel/CrudColumn.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* @method self priority(int $value)
* @method self key(string $value)
* @method self upload(bool $value)
* @method self linkTo(string $routeName, ?string $target = null)
tabacitu marked this conversation as resolved.
Show resolved Hide resolved
*/
class CrudColumn
{
Expand Down
5 changes: 1 addition & 4 deletions src/app/Library/CrudPanel/Traits/Relationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,8 @@ private function modelMethodIsRelationship($model, $method)
/**
* Check if it's possible that attribute is in the relation string when
* the last part of the string is not a method on the chained relations.
*
* @param array $field
* @return bool
*/
private function isAttributeInRelationString($field)
public function isAttributeInRelationString(array $field): bool
{
if (! str_contains($field['entity'], '.')) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,18 @@ public function callRegisteredAttributeMacros(): self

foreach (array_keys($macros) as $macro) {
if (isset($attributes[$macro])) {
is_array($attributes[$macro]) ? $this->{$macro}($attributes[$macro]) : $this->{$macro}([]);
continue;
$this->{$macro}($attributes[$macro]);
}
if (isset($attributes['subfields'])) {
$subfieldsWithMacros = collect($attributes['subfields'])
->filter(fn ($item) => isset($item[$macro]));

$subfieldsWithMacros->each(
function ($item) use ($subfieldsWithMacros, $macro) {
$config = ! is_array($item[$macro]) ? [] : $item[$macro];
if ($subfieldsWithMacros->last() === $item) {
$this->{$macro}($config, $item);
$this->{$macro}($item[$macro], $item);
} else {
$this->{$macro}($config, $item, false);
$this->{$macro}($item[$macro], $item, false);
}
}
);
Expand Down
57 changes: 57 additions & 0 deletions src/macros.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
}
if (! CrudColumn::hasMacro('withFiles')) {
CrudColumn::macro('withFiles', function ($uploadDefinition = [], $subfield = null, $registerUploaderEvents = true) {
$uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : [];
/** @var CrudField|CrudColumn $this */
RegisterUploadEvents::handle($this, $uploadDefinition, 'withFiles', $subfield, $registerUploaderEvents);

Expand All @@ -45,13 +46,69 @@

if (! CrudField::hasMacro('withFiles')) {
CrudField::macro('withFiles', function ($uploadDefinition = [], $subfield = null, $registerUploaderEvents = true) {
$uploadDefinition = is_array($uploadDefinition) ? $uploadDefinition : [];
/** @var CrudField|CrudColumn $this */
RegisterUploadEvents::handle($this, $uploadDefinition, 'withFiles', $subfield, $registerUploaderEvents);

return $this;
});
}

if (! CrudColumn::hasMacro('linkTo')) {
CrudColumn::macro('linkTo', function (string|array|Closure $routeOrConfiguration, ?array $parameters = []): static {
$wrapper = $this->attributes['wrapper'] ?? [];
if (is_array($routeOrConfiguration)) {
$route = $routeOrConfiguration['route'] ?? null;
$parameters = $routeOrConfiguration['parameters'] ?? [];
} else {
$route = $routeOrConfiguration;
}

if ($route instanceof Closure) {
$wrapper['href'] = function ($crud, $column, $entry, $related_key) use ($route) {
return $route($entry, $related_key, $column, $crud);
};
$this->wrapper($wrapper);

return $this;
}

$routeInstance = Route::getRoutes()->getByName($route);

if (! $routeInstance) {
throw new \Exception("Route [{$route}] not found while building the link for column [{$this->attributes['name']}].");
}

$expectedParameters = $routeInstance->parameterNames();

$parameters = (function () use ($parameters, $expectedParameters, $route) {
if (count($expectedParameters) === 0) {
return $parameters;
}
$autoInferedParameter = array_diff($expectedParameters, array_keys($parameters));
if (count($autoInferedParameter) > 1) {
throw new \Exception("Route [{$route}] expects parameters [".implode(', ', $expectedParameters)."]. Insuficient parameters provided in column: [{$this->attributes['name']}].");
}
$autoInferedParameter = current($autoInferedParameter) ? [current($autoInferedParameter) => function ($entry, $related_key, $column, $crud) {
$entity = $crud->isAttributeInRelationString($column) ? Str::before($column['entity'], '.') : $column['entity'];

return $related_key ?? $entry->{$entity}?->getKey();
}] : [];

return array_merge($autoInferedParameter, $parameters);
})();

$wrapper['href'] = function ($crud, $column, $entry, $related_key) use ($route, $parameters) {
$parameters = collect($parameters)->map(fn ($item) => is_callable($item) ? $item($entry, $related_key, $column, $crud) : $item)->toArray();

return route($route, $parameters);
};
$this->wrapper($wrapper);

return $this;
});
}

/**
* The route macro allows developers to generate the routes for a CrudController,
* for all operations, using a simple syntax: Route::crud().
Expand Down
2 changes: 2 additions & 0 deletions tests/BaseTestClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ protected function setUp(): void
'prefix' => config('backpack.base.route_prefix', 'admin'),
],
function () {
Route::get('articles/{id}/show/{detail}', ['as' => 'article.show.detail', 'action' => 'Backpack\CRUD\Tests\config\Http\Controllers\ArticleCrudController@detail']);
Route::crud('users', 'Backpack\CRUD\Tests\config\Http\Controllers\UserCrudController');
Route::crud('articles', 'Backpack\CRUD\Tests\config\Http\Controllers\ArticleCrudController');
}
);
}
Expand Down
147 changes: 147 additions & 0 deletions tests/Unit/CrudPanel/CrudPanelColumnsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Backpack\CRUD\Tests\Unit\CrudPanel;

use Backpack\CRUD\app\Library\CrudPanel\CrudColumn;
use Backpack\CRUD\Tests\config\Models\Article;
use Backpack\CRUD\Tests\config\Models\User;

/**
Expand Down Expand Up @@ -633,4 +634,150 @@ public function testItCanAddAFluentColumnUsingArrayWithoutName()
$this->crudPanel->column(['type' => 'text']);
$this->assertCount(1, $this->crudPanel->columns());
}

public function testColumnLinkToThrowsExceptionWhenNotAllRequiredParametersAreFilled()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Route [article.show.detail] expects parameters [id, detail]. Insuficient parameters provided in column: [articles].');
$this->crudPanel->column('articles')->entity('articles')->linkTo('article.show.detail', ['test' => 'testing']);
}

public function testItThrowsExceptionIfRouteNotFound()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Route [users.route.doesnt.exist] not found while building the link for column [id].');

CrudColumn::name('id')->linkTo('users.route.doesnt.exist')->toArray();
}

public function testColumnLinkToWithRouteNameOnly()
{
$this->crudPanel->column('articles')->entity('articles')->linkTo('articles.show');
$columnArray = $this->crudPanel->columns()['articles'];
$reflection = new \ReflectionFunction($columnArray['wrapper']['href']);
$arguments = $reflection->getClosureUsedVariables();
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('articles.show', $arguments['route']);
$this->assertCount(1, $arguments['parameters']);
$this->assertEquals('http://localhost/admin/articles/1/show', $url);
}

public function testColumnLinkToWithRouteNameAndAdditionalParameters()
{
$this->crudPanel->column('articles')->entity('articles')->linkTo('articles.show', ['test' => 'testing', 'test2' => 'testing2']);
$columnArray = $this->crudPanel->columns()['articles'];
$reflection = new \ReflectionFunction($columnArray['wrapper']['href']);
$arguments = $reflection->getClosureUsedVariables();
$this->assertEquals('articles.show', $arguments['route']);
$this->assertCount(3, $arguments['parameters']);
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/1/show?test=testing&test2=testing2', $url);
}

public function testColumnLinkToWithCustomParameters()
{
$this->crudPanel->column('articles')->entity('articles')->linkTo('article.show.detail', ['detail' => 'testing', 'otherParam' => 'test']);
$columnArray = $this->crudPanel->columns()['articles'];
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/1/show/testing?otherParam=test', $url);
}

public function testColumnLinkToWithCustomClosureParameters()
{
$this->crudPanel->column('articles')
->entity('articles')
->linkTo('article.show.detail', ['detail' => fn ($entry, $related_key) => $related_key, 'otherParam' => fn ($entry) => $entry->content]);
$columnArray = $this->crudPanel->columns()['articles'];
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/1/show/1?otherParam=Some%20Content', $url);
}

public function testColumnLinkToDontAutoInferParametersIfAllProvided()
{
$this->crudPanel->column('articles')
->entity('articles')
->linkTo('article.show.detail', ['id' => 123, 'detail' => fn ($entry, $related_key) => $related_key, 'otherParam' => fn ($entry) => $entry->content]);
$columnArray = $this->crudPanel->columns()['articles'];
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/123/show/1?otherParam=Some%20Content', $url);
}

public function testColumnLinkToAutoInferAnySingleParameter()
{
$this->crudPanel->column('articles')
->entity('articles')
->linkTo('article.show.detail', ['id' => 123, 'otherParam' => fn ($entry) => $entry->content]);
$columnArray = $this->crudPanel->columns()['articles'];
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/123/show/1?otherParam=Some%20Content', $url);
}

public function testColumnLinkToWithClosure()
{
$this->crudPanel->column('articles')
->entity('articles')
->linkTo(fn ($entry) => route('articles.show', $entry->content));
$columnArray = $this->crudPanel->columns()['articles'];
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/Some%20Content/show', $url);
}

public function testColumnArrayDefinitionLinkToRouteAsClosure()
{
$this->crudPanel->setModel(User::class);
$this->crudPanel->column([
'name' => 'articles',
'entity' => 'articles',
'linkTo' => fn ($entry) => route('articles.show', ['id' => $entry->id, 'test' => 'testing']),
]);
$columnArray = $this->crudPanel->columns()['articles'];
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/1/show?test=testing', $url);
}

public function testColumnArrayDefinitionLinkToRouteNameOnly()
{
$this->crudPanel->setModel(User::class);
$this->crudPanel->column([
'name' => 'articles',
'entity' => 'articles',
'linkTo' => 'articles.show',
]);
$columnArray = $this->crudPanel->columns()['articles'];
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/1/show', $url);
}

public function testColumnArrayDefinitionLinkToRouteNameAndAdditionalParameters()
{
$this->crudPanel->setModel(User::class);
$this->crudPanel->column([
'name' => 'articles',
'entity' => 'articles',
'linkTo' => [
'route' => 'articles.show',
'parameters' => [
'test' => 'testing',
'test2' => fn ($entry) => $entry->content,
],
],
]);
$columnArray = $this->crudPanel->columns()['articles'];
$reflection = new \ReflectionFunction($columnArray['wrapper']['href']);
$arguments = $reflection->getClosureUsedVariables();
$this->assertEquals('articles.show', $arguments['route']);
$this->assertCount(3, $arguments['parameters']);
$this->crudPanel->entry = Article::first();
$url = $columnArray['wrapper']['href']($this->crudPanel, $columnArray, $this->crudPanel->entry, 1);
$this->assertEquals('http://localhost/admin/articles/1/show?test=testing&test2=Some%20Content', $url);
}
}
34 changes: 34 additions & 0 deletions tests/config/Http/Controllers/ArticleCrudController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Backpack\CRUD\Tests\Config\Http\Controllers;

use Backpack\CRUD\app\Http\Controllers\CrudController;
use Backpack\CRUD\Tests\config\Models\Article;

class ArticleCrudController extends CrudController
{
use \Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\UpdateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\ListOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\ShowOperation;

public function setup()
{
$this->crud->setModel(Article::class);
$this->crud->setRoute('articles');
}

public function setupUpdateOperation()
{
}

protected function create()
{
return response('create');
}

protected function detail()
{
return response('detail');
}
}
1 change: 1 addition & 0 deletions tests/config/Http/Controllers/UserCrudController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class UserCrudController extends CrudController
use \Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\UpdateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\ListOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\ShowOperation;

public function setup()
{
Expand Down