Skip to content

Commit

Permalink
feature #5852 Make *-To-Many relations sortable (chapterjason)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 4.x branch.

Discussion
----------

Make *-To-Many relations sortable

In a project I've a several *-To-Many relations which I need to be sortable, to have better insight into the data.

I tested One-To-Many and Many-To-Many and also the `setDefaultSort` in `configureCrud`, works as expected.
~~Haven't tested ManyToMany, but I think they should work, too.~~ Didn't worked, but I found a solution.

As it wasn't working before, I made it Opt-In.

- [x] Add tests **Can Someone help me out here? Not sure about the testing structure.**
- [x] Update docs? (Removed the limits so far, I don't think there is something we could add)
- [x] Test ManyToMany

Cheers

## Many-To-Many

### Foo

<img width="1553" alt="Screenshot 2023-07-23 at 8 16 37 PM" src="https://github.com/EasyCorp/EasyAdminBundle/assets/1337562/28fcebc6-c4d2-4a96-943e-f41f9df38601">

<img width="1553" alt="Screenshot 2023-07-23 at 8 16 48 PM" src="https://github.com/EasyCorp/EasyAdminBundle/assets/1337562/b26eed84-4737-4faa-aab7-2e8492cb08ac">

### Bar

<img width="1553" alt="Screenshot 2023-07-23 at 8 17 27 PM" src="https://github.com/EasyCorp/EasyAdminBundle/assets/1337562/ad71e64f-b379-4a0d-beea-296f8f9add8a">

<img width="1553" alt="Screenshot 2023-07-23 at 8 17 15 PM" src="https://github.com/EasyCorp/EasyAdminBundle/assets/1337562/afe5b625-6ed8-4fce-9e9a-14d752267481">

## One-To-Many

<img width="1553" alt="Screenshot 2023-07-23 at 8 18 25 PM" src="https://github.com/EasyCorp/EasyAdminBundle/assets/1337562/1991e589-4a4f-4455-84c4-28b2346b4807">

<img width="1553" alt="Screenshot 2023-07-23 at 8 18 41 PM" src="https://github.com/EasyCorp/EasyAdminBundle/assets/1337562/b2d62fb7-fc84-4bba-88e8-c00cdbed0dd5">

Commits
-------

3167f9e Make *-To-Many relations sortable
  • Loading branch information
javiereguiluz committed Aug 2, 2023
2 parents 8968ad7 + 3167f9e commit 5301e3f
Show file tree
Hide file tree
Showing 16 changed files with 860 additions and 5 deletions.
2 changes: 1 addition & 1 deletion doc/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ Misc. Options

TextField::new('firstName', 'Name')
// if TRUE, listing can be sorted by this field (default: TRUE)
// unmapped fields and Doctrine associations cannot be sorted
// unmapped fields cannot be sorted
->setSortable(false)

// help message displayed for this field in the 'detail', 'edit' and 'new' pages
Expand Down
3 changes: 0 additions & 3 deletions src/Field/Configurator/AssociationConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,6 @@ private function configureToManyAssociation(FieldDto $field): void
{
$field->setCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE, 'toMany');

// associations different from *-to-one cannot be sorted
$field->setSortable(false);

$field->setFormTypeOptionIfNotSet('multiple', true);

/* @var PersistentCollection $collection */
Expand Down
29 changes: 28 additions & 1 deletion src/Orm/EntityRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
Expand Down Expand Up @@ -145,7 +146,33 @@ private function addOrderClause(QueryBuilder $queryBuilder, SearchDto $searchDto
}

if (1 === \count($sortFieldParts)) {
$queryBuilder->addOrderBy('entity.'.$sortProperty, $sortOrder);
if ($entityDto->isToManyAssociation($sortProperty)) {
$metadata = $entityDto->getPropertyMetadata($sortProperty);

/** @var EntityManagerInterface $entityManager */
$entityManager = $this->doctrine->getManagerForClass($entityDto->getFqcn());
$countQueryBuilder = $entityManager->createQueryBuilder();

if (ClassMetadataInfo::MANY_TO_MANY === $metadata->get('type')) {
// many-to-many relation
$countQueryBuilder
->select($queryBuilder->expr()->count('subQueryEntity'))
->from($entityDto->getFqcn(), 'subQueryEntity')
->join(sprintf('subQueryEntity.%s', $sortProperty), 'relatedEntity')
->where('subQueryEntity = entity');
} else {
// one-to-many relation
$countQueryBuilder
->select($queryBuilder->expr()->count('subQueryEntity'))
->from($metadata->get('targetEntity'), 'subQueryEntity')
->where(sprintf('subQueryEntity.%s = entity', $metadata->get('mappedBy')));
}

$queryBuilder->addSelect(sprintf('(%s) as HIDDEN sub_query_sort', $countQueryBuilder->getDQL()));
$queryBuilder->addOrderBy('sub_query_sort', $sortOrder);
} else {
$queryBuilder->addOrderBy('entity.'.$sortProperty, $sortOrder);
}
} else {
$queryBuilder->addOrderBy($sortProperty, $sortOrder);
}
Expand Down
88 changes: 88 additions & 0 deletions tests/Orm/BillSortTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Orm;

use EasyCorp\Bundle\EasyAdminBundle\Test\AbstractCrudTestCase;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\DashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\Sort\BillCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\Bill;

class BillSortTest extends AbstractCrudTestCase
{
private $repository;

protected function getControllerFqcn(): string
{
return BillCrudController::class;
}

protected function getDashboardFqcn(): string
{
return DashboardController::class;
}

protected function setUp(): void
{
parent::setUp();
$this->client->followRedirects();
$this->repository = $this->entityManager->getRepository(Bill::class);
}

/**
* @dataProvider sorting
*/
public function testSorting(array $query, ?string $sortFunction, string $expectedSortIcon)
{
// Arrange
$expectedAmountMapping = [];

/**
* @var Bill $entity
*/
foreach ($this->repository->findAll() as $entity) {
$expectedAmountMapping[$entity->getName()] = $entity->getCustomers()->count();
}

if (null !== $sortFunction) {
$sortFunction($expectedAmountMapping);
}

// Act
$crawler = $this->client->request('GET', $this->generateIndexUrl().'&'.http_build_query($query));

// Assert
$this->assertResponseIsSuccessful();
$this->assertSelectorTextSame('th.header-for-field-association > a', 'Customers');
$this->assertSelectorExists('th.header-for-field-association i.'.$expectedSortIcon);

$index = 1;

foreach ($expectedAmountMapping as $expectedName => $expectedValue) {
$expectedRow = $index++;

$this->assertSelectorTextSame('tbody tr:nth-child('.$expectedRow.') td:nth-child(2)', $expectedName, sprintf('Expected "%s" in row %d', $expectedName, $expectedRow));
$this->assertSelectorTextSame('tbody tr:nth-child('.$expectedRow.') td:nth-child(3)', $expectedValue, sprintf('Expected "%s" in row %d', $expectedValue, $expectedRow));
}
}

public function sorting(): \Generator
{
yield [
[],
null,
'fa-sort',
];

yield [
['sort' => ['customers' => 'ASC']],
'asort',
'fa-arrow-up',
];

yield [
['sort' => ['customers' => 'DESC']],
'arsort',
'fa-arrow-down',
];
}
}
88 changes: 88 additions & 0 deletions tests/Orm/CustomerSortTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Orm;

use EasyCorp\Bundle\EasyAdminBundle\Test\AbstractCrudTestCase;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\DashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\Sort\CustomerCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\Customer;

class CustomerSortTest extends AbstractCrudTestCase
{
private $repository;

protected function getControllerFqcn(): string
{
return CustomerCrudController::class;
}

protected function getDashboardFqcn(): string
{
return DashboardController::class;
}

protected function setUp(): void
{
parent::setUp();
$this->client->followRedirects();
$this->repository = $this->entityManager->getRepository(Customer::class);
}

/**
* @dataProvider sorting
*/
public function testSorting(array $query, ?string $sortFunction, string $expectedSortIcon)
{
// Arrange
$expectedAmountMapping = [];

/**
* @var Customer $entity
*/
foreach ($this->repository->findAll() as $entity) {
$expectedAmountMapping[$entity->getName()] = $entity->getBills()->count();
}

if (null !== $sortFunction) {
$sortFunction($expectedAmountMapping);
}

// Act
$crawler = $this->client->request('GET', $this->generateIndexUrl().'&'.http_build_query($query));

// Assert
$this->assertResponseIsSuccessful();
$this->assertSelectorTextSame('th.header-for-field-association > a', 'Bills');
$this->assertSelectorExists('th.header-for-field-association i.'.$expectedSortIcon);

$index = 1;

foreach ($expectedAmountMapping as $expectedName => $expectedValue) {
$expectedRow = $index++;

$this->assertSelectorTextSame('tbody tr:nth-child('.$expectedRow.') td:nth-child(2)', $expectedName, sprintf('Expected "%s" in row %d', $expectedName, $expectedRow));
$this->assertSelectorTextSame('tbody tr:nth-child('.$expectedRow.') td:nth-child(3)', $expectedValue, sprintf('Expected "%s" in row %d', $expectedValue, $expectedRow));
}
}

public function sorting(): \Generator
{
yield [
[],
null,
'fa-sort',
];

yield [
['sort' => ['bills' => 'ASC']],
'asort',
'fa-arrow-up',
];

yield [
['sort' => ['bills' => 'DESC']],
'arsort',
'fa-arrow-down',
];
}
}
88 changes: 88 additions & 0 deletions tests/Orm/PageSortTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Orm;

use EasyCorp\Bundle\EasyAdminBundle\Test\AbstractCrudTestCase;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\DashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\Sort\PageCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\Page;

class PageSortTest extends AbstractCrudTestCase
{
private $repository;

protected function getControllerFqcn(): string
{
return PageCrudController::class;
}

protected function getDashboardFqcn(): string
{
return DashboardController::class;
}

protected function setUp(): void
{
parent::setUp();
$this->client->followRedirects();
$this->repository = $this->entityManager->getRepository(Page::class);
}

/**
* @dataProvider sorting
*/
public function testSorting(array $query, ?string $sortFunction, string $expectedSortIcon)
{
// Arrange
$expectedAmountMapping = [];

/**
* @var Page $entity
*/
foreach ($this->repository->findAll() as $entity) {
$expectedAmountMapping[$entity->getName()] = $entity->getWebsite()->getName();
}

if (null !== $sortFunction) {
$sortFunction($expectedAmountMapping);
}

// Act
$crawler = $this->client->request('GET', $this->generateIndexUrl().'&'.http_build_query($query));

// Assert
$this->assertResponseIsSuccessful();
$this->assertSelectorTextSame('th.header-for-field-association > a', 'Website');
$this->assertSelectorExists('th.header-for-field-association i.'.$expectedSortIcon);

$index = 1;

foreach ($expectedAmountMapping as $expectedName => $expectedValue) {
$expectedRow = $index++;

$this->assertSelectorTextSame('tbody tr:nth-child('.$expectedRow.') td:nth-child(2)', $expectedName, sprintf('Expected "%s" in row %d', $expectedName, $expectedRow));
$this->assertSelectorTextSame('tbody tr:nth-child('.$expectedRow.') td:nth-child(3)', $expectedValue, sprintf('Expected "%s" in row %d', $expectedValue, $expectedRow));
}
}

public function sorting(): \Generator
{
yield [
[],
null,
'fa-sort',
];

yield [
['sort' => ['website' => 'ASC']],
'asort',
'fa-arrow-up',
];

yield [
['sort' => ['website' => 'DESC']],
'arsort',
'fa-arrow-down',
];
}
}
Loading

0 comments on commit 5301e3f

Please sign in to comment.