Layout of saved export page

This commit is contained in:
Julien Fastré 2025-04-14 10:59:31 +02:00
parent 9f12b42961
commit 3d9b9ea672
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
8 changed files with 143 additions and 61 deletions

View File

@ -18,6 +18,7 @@ use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Chill\MainBundle\Export\GroupedExportInterface;
use Chill\MainBundle\Form\SavedExportType;
use Chill\MainBundle\Repository\ExportGenerationRepository;
use Chill\MainBundle\Repository\SavedExportRepositoryInterface;
use Chill\MainBundle\Security\Authorization\ExportGenerationVoter;
use Chill\MainBundle\Security\Authorization\SavedExportVoter;
@ -46,6 +47,7 @@ final readonly class SavedExportController
private Security $security,
private TranslatorInterface $translator,
private UrlGeneratorInterface $urlGenerator,
private ExportGenerationRepository $exportGenerationRepository,
) {}
#[Route(path: '/{_locale}/exports/saved/{id}/delete', name: 'chill_main_export_saved_delete')]
@ -172,7 +174,10 @@ final readonly class SavedExportController
throw new AccessDeniedHttpException();
}
$exports = $this->savedExportRepository->findByUser($user, ['title' => 'ASC']);
$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 */
@ -189,12 +194,20 @@ final readonly class SavedExportController
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

@ -13,8 +13,10 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\SavedExport;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
/**
@ -55,6 +57,30 @@ class SavedExportRepository implements SavedExportRepositoryInterface
->where($qb->expr()->eq('se.user', ':user'))
->setParameter('user', $user);
return $this->prepareResult($qb, $orderBy, $limit, $offset);
}
public function findSharedWithUser(User $user, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
{
$qb = $this->repository->createQueryBuilder('se');
$qb
->where(
$qb->expr()->orX(
$qb->expr()->eq('se.user', ':user'),
$qb->expr()->isMemberOf(':user', 'se.sharedWithUsers'),
$qb->expr()->exists(
sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF se.sharedWithGroups AND :user MEMBER OF ug.users', UserGroup::class)
)
)
)
->setParameter('user', $user);
return $this->prepareResult($qb, $orderBy, $limit, $offset);
}
private function prepareResult(QueryBuilder $qb, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
{
if (null !== $limit) {
$qb->setMaxResults($limit);
}

View File

@ -34,6 +34,8 @@ interface SavedExportRepositoryInterface extends ObjectRepository
*/
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;
public function findOneBy(array $criteria): ?SavedExport;
public function getClassName(): string;

View File

@ -392,4 +392,24 @@ Toutes les classes btn-* de bootstrap sont fonctionnelles
</div>
</div>
<h1>Badges</h1>
<p>
<span class="badge bg-primary">Primary</span>
<span class="badge bg-secondary">Secondary</span>
<span class="badge bg-success">Success</span>
<span class="badge bg-danger">Danger</span>
<span class="badge bg-warning">Warning</span>
<span class="badge bg-info">Info</span>
<span class="badge bg-light">Light</span>
<span class="badge bg-dark">Dark</span>
<span class="badge bg-chill-blue">chill-blue</span>
<span class="badge bg-chill-green">chill-green</span>
<span class="badge bg-chill-yellow">chill-yellow</span>
<span class="badge bg-chill-orange">chill-orange</span>
<span class="badge bg-chill-red">chill-red</span>
<span class="badge bg-chill-beige">chill-beige</span>
</p>
{% endblock %}

View File

@ -6,7 +6,7 @@
</li>
<li class="nav-item">
<a href="{{ chill_path_forward_return_path('chill_main_export_saved_list_my') }}" class="nav-link {% if current == 'my' %}active{% endif %}">
{{ 'saved_export.My saved exports'|trans }}
{{ 'saved_export.Saved exports'|trans }}
</a>
</li>
</ul>

View File

@ -1,14 +1,74 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_saved_export_button') }}
<style lang="css">
.export-title {
margin-top: 2rem;
margin-bottom: 1rem;
}
</style>
{% endblock %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_saved_export_button') }}
{% endblock %}
{% block title %}{{ 'saved_export.My saved exports'|trans }}{% endblock %}
{% block title %}{{ 'saved_export.Saved exports'|trans }}{% endblock %}
{% macro render_export_card(saved, export, export_alias, generations) %}
<div class="col">
<div class="card h-100">
<div class="card-header">
{{ export.title|trans }}
</div>
<div class="card-body">
<h2 class="card-title">{{ saved.title }}</h2>
{% if app.user is same as saved.createdBy %}
<p class="card-text tags">
{% if app.user is same as saved.createdBy %}<span class="badge bg-primary">{{ 'saved_export.Owner'|trans }}</span>{% endif %}
</p>
{% endif %}
<p class="card-text my-3">{{ saved.description|chill_markdown_to_html }}</p>
</div>
{% if generations|length > 0 %}
<ul class="list-group list-group-flush">
{% for generation in generations %}
<li class="list-group-item">
<a href="{{ chill_path_add_return_path('chill_main_export-generation_wait', {'id': generation.id}) }}">{{ generation.createdAt|format_datetime('short', 'short') }}</a>
{% if generation.status == 'pending' %}
&nbsp;<span class="badge bg-info">{{ 'export.generation.Export generation is pending_short'|trans }}</span>
{% elseif generation.status == 'failure' %}
&nbsp;<span class="badge bg-warning">{{ 'export.generation.Error_short'|trans }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
<div class="card-footer">
<ul class="record_actions slim">
<li>
<div class="" data-generate-export-button data-saved-export-uuid="{{ saved.id|escape('html_attr') }}"></div>
</li>
<li>
<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ 'Actions'|trans }}
</button>
<ul class="dropdown-menu">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_title_and_description'|trans }}</a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_new', {'alias': saved.exportAlias,'from_saved': saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_filters_aggregators_and_execute'|trans }}</a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': saved.id }) }}" class="dropdown-item"><i class="fa fa-trash"></i> {{ 'Delete'|trans }}</a></li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
{% endmacro %}
{% block content %}
<div class="col-md-10 exports-list">
@ -23,73 +83,23 @@
{% for group, saveds in grouped_exports %}
{% if group != '_' %}
<h2 class="display-6">{{ group }}</h2>
<div class="row flex-bloc">
<h1 class="display-6 export-title">{{ group }}</h1>
<div class="row row-cols-1 row-cols-md-3 g-2">
{% for s in saveds %}
<div class="item-bloc">
<div class="item-row card-body">
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
<h2 class="card-title">{{ s.saved.title }}</h2>
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
<div class="card-text my-3">
{{ s.saved.description|chill_markdown_to_html }}
</div>
<div>
<ul class="record_actions">
<li>
<div class="" data-generate-export-button data-saved-export-uuid="{{ s.saved.id|escape('html_attr') }}"></div>
</li>
<li>
<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ 'Actions'|trans }}
</button>
<ul class="dropdown-menu">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_title_and_description'|trans }}</a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_new', {'alias': s.saved.exportAlias,'from_saved': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-pencil"></i> {{ 'saved_export.update_filters_aggregators_and_execute'|trans }}</a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="dropdown-item"><i class="fa fa-trash"></i> {{ 'Delete'|trans }}</a></li>
</ul>
</div>
</li>
</ul>
</div>
</div>
</div>
{{ _self.render_export_card(s.saved, s.export, s.saved.exportAlias, last_executions[s.saved.id.toString()]) }}
{% endfor %}
</div>
{% endif %}
{% endfor %}
{% if grouped_exports|keys|length > 1 and grouped_exports['_']|default([])|length > 0 %}
<h2 class="display-6">{{ 'Ungrouped exports'|trans }}</h2>
<h2 class="display-6 export-title">{{ 'Ungrouped exports'|trans }}</h2>
{% endif %}
<div class="row flex-bloc">
{% for saveds in grouped_exports['_']|default([]) %}
{% for s in saveds %}
<div class="item-bloc">
<div class="item-row card-body">
<p class="card-subtitle"><strong>{{ s.export.title|trans }}</strong></p>
<h2 class="card-title">{{ s.saved.title }}</h2>
<div class="createdBy">{{ 'saved_export.Created on %date%'|trans({'%date%': s.saved.createdAt|format_datetime('long', 'short')}) }}</div>
<div class="card-text my-3">
{{ s.saved.description|chill_markdown_to_html }}
</div>
<div>
<ul class="record_actions">
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_delete', {'id': s.saved.id }) }}" class="btn btn-delete"></a></li>
<li><a href="{{ chill_path_add_return_path('chill_main_export_saved_edit', {'id': s.saved.id }) }}" class="btn btn-edit"></a></li>
<li><a href="{{ path('chill_main_export_generate_from_saved', { id: s.saved.id }) }}" class="btn btn-action"><i class="fa fa-cog"></i></a></li>
</ul>
</div>
</div>
</div>
{{ _self.render_export_card(s.saved, s.export, s.saved.exportAlias, last_executions[s.saved.id.toString()]) }}
{% endfor %}
{% endfor %}
</div>

View File

@ -11,6 +11,9 @@ declare(strict_types=1);
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Export\DirectExportInterface;
use Chill\MainBundle\Export\ExportInterface;
use Chill\MainBundle\Export\ExportManager;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
@ -20,7 +23,7 @@ class ChillExportVoter extends Voter
private readonly VoterHelperInterface $helper;
public function __construct(VoterHelperFactoryInterface $voterHelperFactory)
public function __construct(VoterHelperFactoryInterface $voterHelperFactory, ExportManager $exportManager)
{
$this->helper = $voterHelperFactory
->generate(self::class)
@ -30,6 +33,13 @@ class ChillExportVoter extends Voter
protected function supports($attribute, $subject): bool
{
if (
($subject instanceof ExportInterface or $subject instanceof DirectExportInterface)
&& $attribute === $subject->requiredRole()
) {
return true;
}
return $this->helper->supports($attribute, $subject);
}

View File

@ -787,7 +787,7 @@ saved_export:
Edit: Modifier un export enregistré
Delete saved ?: Supprimer un export enregistré ?
Are you sure you want to delete this saved ?: Êtes-vous sûr·e de vouloir supprimer cet export ?
My saved exports: Mes exports enregistrés
Saved exports: Exports enregistrés
Export is deleted: L'export est supprimé
Saved export is saved!: L'export est enregistré
Created on %date%: Créé le %date%
@ -795,6 +795,7 @@ saved_export:
update_filters_aggregators_and_execute: Modifier les filtres et regroupements et télécharger
execute: Générer
Update existing: Mettre à jour le rapport enregistré existant
Owner: Propriétaire
absence:
# single letter for absence