Add duplicate and update options for saved exports

Introduce functionality to duplicate saved exports and update options directly from export generations. Update translations, controllers, views, and entities to support the new features, providing better flexibility and user experience around saved export management.
This commit is contained in:
Julien Fastré 2025-04-23 09:11:50 +02:00
parent 0f6b10aa0a
commit 973450110b
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
6 changed files with 110 additions and 8 deletions

View File

@ -31,6 +31,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;
@ -104,14 +105,50 @@ final readonly class SavedExportController
->setExportAlias($exportGeneration->getExportAlias())
->setUser($user)
->setOptions($exportGeneration->getOptions())
->setTitle($this->translator->trans($this->exportManager->getExport($exportGeneration->getExportAlias())->getTitle()));
->setTitle(
$request->query->has('title') ?
$request->query->get('title') :
$this->translator->trans($this->exportManager->getExport($exportGeneration->getExportAlias())->getTitle())
);
return $this->handleEdit($savedExport, $request);
}
#[Route(path: '/exports/saved/duplicate-from-saved-export/{id}/new', name: 'chill_main_export_saved_duplicate')]
public function duplicate(SavedExport $previousSavedExport, Request $request): Response
{
if (!$this->security->isGranted(SavedExportVoter::GENERATE, $previousSavedExport)) {
throw new AccessDeniedHttpException("Not allowed to see this saved export");
}
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException('only regular user can create a saved export');
}
$savedExport = new SavedExport();
$savedExport
->setExportAlias($previousSavedExport->getExportAlias())
->setUser($user)
->setOptions($previousSavedExport->getOptions())
->setDescription($previousSavedExport->getDescription())
->setTitle(
$request->query->has('title') ?
$request->query->get('title') :
$previousSavedExport->getTitle() . ' (' . $this->translator->trans('saved_export.Duplicated') . ' ' . (new \DateTimeImmutable('now'))->format('d-m-Y H:i:s') . ')'
);
return $this->handleEdit($savedExport, $request);
}
private function handleEdit(SavedExport $savedExport, Request $request): Response
{
$form = $this->formFactory->create(SavedExportType::class, $savedExport);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($savedExport);
$exportGeneration->setSavedExport($savedExport);
$this->entityManager->flush();
if (($session = $request->getSession()) instanceof Session) {
@ -166,6 +203,35 @@ final readonly class SavedExportController
);
}
#[Route(path: '/{_locale}/exports/saved/{savedExport}/edit-options/{exportGeneration}', name: 'chill_main_export_saved_options_edit')]
public function updateOptionsFromGeneration(SavedExport $savedExport, ExportGeneration $exportGeneration, Request $request): Response
{
if (!$this->security->isGranted(SavedExportVoter::EDIT, $savedExport)) {
throw new AccessDeniedHttpException("You are not allowed to access this saved export");
}
if (!$this->security->isGranted(ExportGenerationVoter::VIEW, $exportGeneration)) {
throw new AccessDeniedHttpException("You are not allowed to access this export generation");
}
if ($savedExport->getExportAlias() !== $exportGeneration->getExportAlias()) {
throw new UnprocessableEntityHttpException("export alias does not match");
}
$savedExport->setOptions($exportGeneration->getOptions());
$this->entityManager->flush();
$session = $request->getSession();
if ($session instanceof Session) {
$session->getFlashBag()->add('success', new TranslatableMessage("saved_export.Options updated successfully"));
}
return new RedirectResponse(
$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
{

View File

@ -113,6 +113,11 @@ class ExportGeneration implements TrackCreationInterface
return $this;
}
public function isLinkedToSavedExport(): bool
{
return null !== $this->savedExport;
}
public static function fromSavedExport(SavedExport $savedExport, ?\DateTimeImmutable $deletedAt = null): self
{
$generation = new self($savedExport->getExportAlias(), $savedExport->getOptions(), $deletedAt, $savedExport);

View File

@ -23,10 +23,34 @@
{{ 'export.generation.Come back later'|trans|chill_return_path_label }}
</a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_main_export_saved_create_from_export_generation', {'id': exportGeneration.id}) }}" class="btn btn-save">
{{ 'Save'|trans }}
</a>
</li>
{% if not exportGeneration.isLinkedToSavedExport %}
<li>
<a href="{{ chill_path_add_return_path('chill_main_export_saved_create_from_export_generation', {'id': exportGeneration.id}) }}" class="btn btn-save">
{{ 'Save'|trans }}
</a>
</li>
{% else %}
<li>
<div class="dropdown">
<button class="btn btn-save dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">{{ 'Save'|trans }}</button>
<ul class="dropdown-menu dropdown-menu-end">
<li class="dropdown-item">
<a href="{{ chill_path_add_return_path('chill_main_export_saved_create_from_export_generation', {'id': exportGeneration.id, 'title': exportGeneration.savedExport.title ~ ' (' ~ 'saved_export.Duplicated'|trans ~ ' ' ~ null|format_datetime('short', 'medium') ~ ')'}) }}" class="btn">
<i class="bi bi-copy"></i> {{ 'saved_export.Save to new saved export'|trans }}
</a>
</li>
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_EDIT', exportGeneration.savedExport) %}
<li class="dropdown-item">
<form method="POST" action="{{ path('chill_main_export_saved_options_edit', {'savedExport': exportGeneration.savedExport.id, 'exportGeneration': exportGeneration.id }) }}">
<button type="submit" class="btn">
<i class="bi bi-floppy"></i> {{ 'saved_export.Update current saved export'|trans }}
</button>
</form>
</li>
{% endif %}
</ul>
</div>
</li>
{% endif %}
</ul>
{% endblock content %}

View File

@ -26,7 +26,7 @@
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_main_homepage') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
<a href="{{ chill_return_path_or('chill_main_export_saved_list_my') }}" class="btn btn-cancel">{{ 'Cancel'|trans }}</a>
</li>
<li>
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>

View File

@ -63,6 +63,7 @@
{% endif %}
{# reminder: the controller already checked that the user can generate saved exports #}
<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_duplicate', {'id': saved.id}) }}" class="dropdown-item"><i class="fa fa-copy"></i> {{ 'saved_export.Duplicate'|trans }}</a></li>
{% if is_granted('CHILL_MAIN_EXPORT_SAVED_DELETE', saved) %}
<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>
{% endif %}

View File

@ -800,6 +800,12 @@ saved_export:
execute: Générer
Update existing: Mettre à jour le rapport enregistré existant
Owner: Propriétaire
Save to new saved export: Créer un nouvel export enregistré
Update current saved export: Modifier la configuration de l'export existant
Duplicate: Dupliquer
Duplicated: Dupliqué
Options updated successfully: La configuration de l'export a été mise à jour
Share: Partage
absence:
# single letter for absence