configurable csv/xlsx export templates (#5531)
This commit is contained in:
43
src/Form/ExportTemplateSpreadsheetForm.php
Normal file
43
src/Form/ExportTemplateSpreadsheetForm.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\ExportTemplate;
|
||||
use App\Form\Type\ExportColumnsType;
|
||||
use App\Form\Type\ExportRendererType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ExportTemplateSpreadsheetForm extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('title', TextType::class);
|
||||
$builder->add('renderer', ExportRendererType::class, ['label' => 'type']);
|
||||
$builder->add('language', LanguageType::class, ['required' => false]);
|
||||
$builder->add('columns', ExportColumnsType::class, ['required' => true]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ExportTemplate::class,
|
||||
'csrf_protection' => true,
|
||||
'csrf_field_name' => '_token',
|
||||
'csrf_token_id' => 'export_template_create',
|
||||
'attr' => [
|
||||
'data-form-event' => 'kimai.exportTemplate'
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -40,18 +40,22 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
|
||||
$extendedOptions = ['class' => 'selectpicker'];
|
||||
|
||||
if ($options['multiple']) {
|
||||
if (\array_key_exists('multiple', $options) && $options['multiple']) {
|
||||
$extendedOptions['size'] = 1;
|
||||
}
|
||||
|
||||
if (false !== $options['width']) {
|
||||
if (\array_key_exists('width', $options) && false !== $options['width']) {
|
||||
$extendedOptions['data-width'] = $options['width'];
|
||||
}
|
||||
|
||||
if (false === $options['search']) {
|
||||
if (\array_key_exists('search', $options) && false === $options['search']) {
|
||||
$extendedOptions['data-disable-search'] = 1;
|
||||
}
|
||||
|
||||
if (\array_key_exists('order', $options) && true === $options['order']) {
|
||||
$extendedOptions['data-order'] = 1;
|
||||
}
|
||||
|
||||
// there is a very weird logic in vendor/symfony/twig-bridge/Resources/views/Form/form_div_layout.html.twig
|
||||
// in block "block choice_widget_collapsed" that resets "{% set required = false %}", so we fake it into the select
|
||||
if (true === $options['required'] && \is_array($options['attr']) && (!\array_key_exists('size', $options['attr']) || $options['attr']['size'] <= 1)) {
|
||||
@@ -75,5 +79,9 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
|
||||
$resolver->setDefined(['search']);
|
||||
$resolver->setAllowedTypes('search', 'boolean');
|
||||
$resolver->setDefault('search', true);
|
||||
|
||||
$resolver->setDefined(['order']);
|
||||
$resolver->setAllowedTypes('order', 'boolean');
|
||||
$resolver->setDefault('order', false);
|
||||
}
|
||||
}
|
||||
|
||||
166
src/Form/Type/ExportColumnsType.php
Normal file
166
src/Form/Type/ExportColumnsType.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Event\ActivityMetaDisplayEvent;
|
||||
use App\Event\CustomerMetaDisplayEvent;
|
||||
use App\Event\MetaDisplayEventInterface;
|
||||
use App\Event\ProjectMetaDisplayEvent;
|
||||
use App\Event\TimesheetMetaDisplayEvent;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Repository\Query\ActivityQuery;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class ExportColumnsType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $ordered = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly TranslatorInterface $translator,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$columns = [
|
||||
'timesheet' => [
|
||||
'date' => 'date',
|
||||
'begin' => 'begin',
|
||||
'end' => 'end',
|
||||
$this->translator->trans('duration') . ' (1:30)' => 'duration',
|
||||
$this->translator->trans('duration') . ' (1:30:00)' => 'duration_seconds',
|
||||
$this->translator->trans('duration') . ' (1.5)' => 'duration_decimal',
|
||||
'currency' => 'currency',
|
||||
'rate' => 'rate',
|
||||
'internalRate' => 'internal_rate',
|
||||
'hourlyRate' => 'hourly_rate',
|
||||
'fixedRate' => 'fixed_rate',
|
||||
'description' => 'description',
|
||||
'exported' => 'exported',
|
||||
'billable' => 'billable',
|
||||
'tags' => 'tags',
|
||||
'type' => 'type',
|
||||
'category' => 'category',
|
||||
],
|
||||
'user' => [
|
||||
'alias' => 'user.alias',
|
||||
'username' => 'user.name',
|
||||
'account_number' => 'user.account_number',
|
||||
],
|
||||
'customer' => [
|
||||
'customer' => 'customer.name',
|
||||
'number' => 'customer.number',
|
||||
'vat_id' => 'customer.vat_id',
|
||||
],
|
||||
'project' => [
|
||||
'project' => 'project.name',
|
||||
'project_number' => 'project.number',
|
||||
'orderNumber' => 'project.order_number',
|
||||
],
|
||||
'activity' => [
|
||||
'activity' => 'activity.name',
|
||||
'activity_number' => 'activity.number',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($this->findMetaColumns(new TimesheetMetaDisplayEvent(new TimesheetQuery(), TimesheetMetaDisplayEvent::EXPORT)) as $metaField) {
|
||||
if ($metaField->getName() !== null) {
|
||||
$columns['timesheet'][$metaField->getLabel()] = 'timesheet.meta.' . $metaField->getName();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->findMetaColumns(new CustomerMetaDisplayEvent(new CustomerQuery(), CustomerMetaDisplayEvent::EXPORT)) as $metaField) {
|
||||
if ($metaField->getName() !== null) {
|
||||
$columns['customer'][$metaField->getLabel()] = 'customer.meta.' . $metaField->getName();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->findMetaColumns(new ProjectMetaDisplayEvent(new ProjectQuery(), ProjectMetaDisplayEvent::EXPORT)) as $metaField) {
|
||||
if ($metaField->getName() !== null) {
|
||||
$columns['project'][$metaField->getLabel()] = 'project.meta.' . $metaField->getName();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->findMetaColumns(new ActivityMetaDisplayEvent(new ActivityQuery(), ActivityMetaDisplayEvent::EXPORT)) as $metaField) {
|
||||
if ($metaField->getName() !== null) {
|
||||
$columns['activity'][$metaField->getLabel()] = 'activity.meta.' . $metaField->getName();
|
||||
}
|
||||
}
|
||||
|
||||
$event = new UserPreferenceDisplayEvent(UserPreferenceDisplayEvent::EXPORT);
|
||||
$this->dispatcher->dispatch($event);
|
||||
foreach ($event->getPreferences() as $metaField) {
|
||||
if ($metaField->getName() !== null) {
|
||||
$columns['user'][$metaField->getLabel()] = 'user.meta.' . $metaField->getName();
|
||||
}
|
||||
}
|
||||
|
||||
$resolver->setDefaults([
|
||||
'choices' => $columns,
|
||||
'label' => 'modal.columns.label',
|
||||
'multiple' => true,
|
||||
// does not work in the frontend
|
||||
//'order' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$data = $event->getData();
|
||||
if (\is_array($data)) {
|
||||
$this->ordered = $data; // @phpstan-ignore assign.propertyType
|
||||
}
|
||||
}
|
||||
);
|
||||
$builder->addEventListener(
|
||||
FormEvents::SUBMIT,
|
||||
function (FormEvent $event) {
|
||||
$event->setData($this->ordered);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<MetaTableTypeInterface>
|
||||
*/
|
||||
private function findMetaColumns(MetaDisplayEventInterface $event): array
|
||||
{
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $event->getFields();
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
29
src/Form/Type/ExportRendererType.php
Normal file
29
src/Form/Type/ExportRendererType.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class ExportRendererType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'choices' => ['button.csv' => 'csv', 'button.xlsx' => 'xlsx'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user