Refactor SavedExport listing to support filtering.

Introduced filtering capabilities for SavedExport listings by title and description. Moved index functionality to a new `SavedExportIndexController` and updated the repository with the necessary filter logic. Adjusted the Twig template to render the new filter interface.
This commit is contained in:
Julien Fastré 2025-05-26 14:16:09 +02:00
parent 9f32b5ac48
commit be448c650e
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
5 changed files with 140 additions and 57 deletions

View File

@ -14,13 +14,8 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\ExportGeneration; use Chill\MainBundle\Entity\ExportGeneration;
use Chill\MainBundle\Entity\SavedExport; use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager; use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Form\SavedExportType; use Chill\MainBundle\Form\SavedExportType;
use Chill\MainBundle\Repository\ExportGenerationRepository;
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\MainBundle\Security\Authorization\ExportGenerationVoter; use Chill\MainBundle\Security\Authorization\ExportGenerationVoter;
use Chill\MainBundle\Security\Authorization\SavedExportVoter; use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -46,11 +41,9 @@ final readonly class SavedExportController
private EntityManagerInterface $entityManager, private EntityManagerInterface $entityManager,
private ExportManager $exportManager, private ExportManager $exportManager,
private FormFactoryInterface $formFactory, private FormFactoryInterface $formFactory,
private SavedExportRepositoryInterface $savedExportRepository,
private Security $security, private Security $security,
private TranslatorInterface $translator, private TranslatorInterface $translator,
private UrlGeneratorInterface $urlGenerator, private UrlGeneratorInterface $urlGenerator,
private ExportGenerationRepository $exportGenerationRepository,
) {} ) {}
#[Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')] #[Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')]
@ -234,52 +227,4 @@ final readonly class SavedExportController
$this->urlGenerator->generate('chill_main_export_saved_edit', ['id' => $savedExport->getId()]), $this->urlGenerator->generate('chill_main_export_saved_edit', ['id' => $savedExport->getId()]),
); );
} }
#[Route(path: '/{_locale}/exports/saved/my', name: 'chill_main_export_saved_list_my')]
public function list(): Response
{
$user = $this->security->getUser();
if (!$this->security->isGranted(ChillExportVoter::GENERATE_SAVED_EXPORT) || !$user instanceof User) {
throw new AccessDeniedHttpException(sprintf('Missing role: %s', ChillExportVoter::GENERATE_SAVED_EXPORT));
}
$exports = array_filter(
$this->savedExportRepository->findSharedWithUser($user, ['exportAlias' => 'ASC', 'title' => 'ASC']),
fn (SavedExport $savedExport): bool => $this->security->isGranted(SavedExportVoter::GENERATE, $savedExport),
);
// group by center
/** @var array<string, array{saved: SavedExport, export: ExportInterface}> $exportsGrouped */
$exportsGrouped = [];
foreach ($exports as $savedExport) {
$export = $this->exportManager->getExport($savedExport->getExportAlias());
$exportsGrouped[
$export instanceof GroupedExportInterface
? $this->translator->trans($export->getGroup()) : '_'
][] = ['saved' => $savedExport, 'export' => $export];
}
ksort($exportsGrouped);
// get last executions
$lastExecutions = [];
foreach ($exports as $savedExport) {
$lastExecutions[$savedExport->getId()->toString()] = $this->exportGenerationRepository
->findExportGenerationBySavedExportAndUser($savedExport, $user, 5);
}
return new Response(
$this->templating->render(
'@ChillMain/SavedExport/index.html.twig',
[
'grouped_exports' => $exportsGrouped,
'total' => \count($exports),
'last_executions' => $lastExecutions,
],
),
);
}
} }

View File

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Repository\ExportGenerationRepository;
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
final readonly class SavedExportIndexController
{
public function __construct(
private \Twig\Environment $templating,
private ExportManager $exportManager,
private SavedExportRepositoryInterface $savedExportRepository,
private Security $security,
private TranslatorInterface $translator,
private ExportGenerationRepository $exportGenerationRepository,
private FilterOrderHelperFactoryInterface $filterOrderHelperFactory,
) {}
#[Route(path: '/{_locale}/exports/saved/my', name: 'chill_main_export_saved_list_my')]
public function list(): Response
{
$user = $this->security->getUser();
if (!$this->security->isGranted(ChillExportVoter::GENERATE_SAVED_EXPORT) || !$user instanceof User) {
throw new AccessDeniedHttpException(sprintf('Missing role: %s', ChillExportVoter::GENERATE_SAVED_EXPORT));
}
$filter = $this->buildFilter();
$filterParams = [];
if ('' !== $filter->getQueryString() && null !== $filter->getQueryString()) {
$filterParams[SavedExportRepositoryInterface::FILTER_DESCRIPTION | SavedExportRepositoryInterface::FILTER_TITLE] = $filter->getQueryString();
}
$exports = array_filter(
$this->savedExportRepository->findSharedWithUser($user, ['exportAlias' => 'ASC', 'title' => 'ASC'], filters: $filterParams),
fn (SavedExport $savedExport): bool => $this->security->isGranted(SavedExportVoter::GENERATE, $savedExport),
);
// group by center
/** @var array<string, array{saved: SavedExport, export: ExportInterface}> $exportsGrouped */
$exportsGrouped = [];
foreach ($exports as $savedExport) {
$export = $this->exportManager->getExport($savedExport->getExportAlias());
$exportsGrouped[$export instanceof GroupedExportInterface
? $this->translator->trans($export->getGroup()) : '_'][] = ['saved' => $savedExport, 'export' => $export];
}
ksort($exportsGrouped);
// get last executions
$lastExecutions = [];
foreach ($exports as $savedExport) {
$lastExecutions[$savedExport->getId()->toString()] = $this->exportGenerationRepository
->findExportGenerationBySavedExportAndUser($savedExport, $user, 5);
}
return new Response(
$this->templating->render(
'@ChillMain/SavedExport/index.html.twig',
[
'grouped_exports' => $exportsGrouped,
'total' => \count($exports),
'last_executions' => $lastExecutions,
'filter' => $filter,
],
),
);
}
private function buildFilter(): FilterOrderHelper
{
$filter = $this->filterOrderHelperFactory->create('saved-export-index-filter');
$filter->addSearchBox();
return $filter->build();
}
}

View File

@ -18,6 +18,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\String\UnicodeString;
/** /**
* @implements ObjectRepository<SavedExport> * @implements ObjectRepository<SavedExport>
@ -60,7 +61,7 @@ class SavedExportRepository implements SavedExportRepositoryInterface
return $this->prepareResult($qb, $orderBy, $limit, $offset); return $this->prepareResult($qb, $orderBy, $limit, $offset);
} }
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array
{ {
$qb = $this->repository->createQueryBuilder('se'); $qb = $this->repository->createQueryBuilder('se');
@ -76,6 +77,27 @@ class SavedExportRepository implements SavedExportRepositoryInterface
) )
->setParameter('user', $user); ->setParameter('user', $user);
foreach ($filters as $key => $filter) {
if (self::FILTER_TITLE === ($key & self::FILTER_TITLE)
|| self::FILTER_DESCRIPTION === ($key & self::FILTER_DESCRIPTION)) {
$filter = new UnicodeString($filter);
$i = 0;
foreach ($filter->split(' ') as $word) {
$orx = $qb->expr()->orX();
if (self::FILTER_TITLE === ($key & self::FILTER_TITLE)) {
$orx->add($qb->expr()->like('LOWER(se.title)', 'LOWER(:qs'.$i.')'));
}
if (self::FILTER_DESCRIPTION === ($key & self::FILTER_DESCRIPTION)) {
$orx->add($qb->expr()->like('LOWER(se.description)', 'LOWER(:qs'.$i.')'));
}
$qb->andWhere($orx);
$qb->setParameter('qs'.$i, '%'.$word->trim().'%');
++$i;
}
}
}
return $this->prepareResult($qb, $orderBy, $limit, $offset); return $this->prepareResult($qb, $orderBy, $limit, $offset);
} }

View File

@ -20,6 +20,9 @@ use Doctrine\Persistence\ObjectRepository;
*/ */
interface SavedExportRepositoryInterface extends ObjectRepository interface SavedExportRepositoryInterface extends ObjectRepository
{ {
public const FILTER_TITLE = 0x01;
public const FILTER_DESCRIPTION = 0x10;
public function find($id): ?SavedExport; public function find($id): ?SavedExport;
/** /**
@ -34,7 +37,14 @@ interface SavedExportRepositoryInterface extends ObjectRepository
*/ */
public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array; public function findByUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array;
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array; /**
* Get the saved export created by and the user and the ones shared with the user.
*
* @param array<int, mixed> $filters filters where keys are one of the constant starting with FILTER_
*
* @return list<SavedExport>
*/
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null, array $filters = []): array;
public function findOneBy(array $criteria): ?SavedExport; public function findOneBy(array $criteria): ?SavedExport;

View File

@ -18,6 +18,7 @@
{% block title %}{{ 'saved_export.Saved exports'|trans }}{% endblock %} {% block title %}{{ 'saved_export.Saved exports'|trans }}{% endblock %}
{% macro render_export_card(saved, export, export_alias, generations) %} {% macro render_export_card(saved, export, export_alias, generations) %}
<div class="col"> <div class="col">
<div class="card h-100"> <div class="card h-100">
@ -88,6 +89,7 @@
{{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }} {{ include('@ChillMain/Export/_navbar.html.twig', {'current' : 'my'}) }}
<div class="container mt-4"> <div class="container mt-4">
{{ filter|chill_render_filter_order_helper }}
{% if total == 0 %} {% if total == 0 %}
<p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p> <p class="chill-no-data-statement" >{{ 'saved_export.Any saved export'|trans }}</p>