Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)

This commit is contained in:
Kevin Papst
2022-12-31 21:19:55 +01:00
committed by GitHub
parent 95e06746bd
commit 90a0fd8a22
2164 changed files with 83700 additions and 86426 deletions

View File

@@ -10,30 +10,7 @@
namespace App\Form\API;
use App\Form\ActivityEditForm;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ActivityApiEditForm extends ActivityEditForm
final class ActivityApiEditForm extends ActivityEditForm
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->remove('metaFields');
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -10,19 +10,7 @@
namespace App\Form\API;
use App\Form\ActivityRateForm;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ActivityRateApiForm extends ActivityRateForm
final class ActivityRateApiForm extends ActivityRateForm
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -10,30 +10,7 @@
namespace App\Form\API;
use App\Form\CustomerEditForm;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerApiEditForm extends CustomerEditForm
final class CustomerApiEditForm extends CustomerEditForm
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->remove('metaFields');
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -10,19 +10,7 @@
namespace App\Form\API;
use App\Form\CustomerRateForm;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerRateApiForm extends CustomerRateForm
final class CustomerRateApiForm extends CustomerRateForm
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -7,19 +7,16 @@
* file that was distributed with this source code.
*/
namespace App\Form\Type;
namespace App\Form\API;
use App\API\BaseApiController;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class APIDateTimeType extends AbstractType
final class DateTimeApiType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'documentation' => [
@@ -28,16 +25,12 @@ class APIDateTimeType extends AbstractType
'example' => (new \DateTime())->format(BaseApiController::DATE_FORMAT_PHP),
],
'widget' => 'single_text',
'html5' => true,
'model_timezone' => date_default_timezone_get(),
'view_timezone' => date_default_timezone_get(),
'html5' => true, // for the correct format
'with_seconds' => false,
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return DateTimeType::class;
}

View File

@@ -10,36 +10,7 @@
namespace App\Form\API;
use App\Form\ProjectEditForm;
use App\Form\Type\APITrueFalseType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectApiEditForm extends ProjectEditForm
final class ProjectApiEditForm extends ProjectEditForm
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->remove('metaFields');
$builder->remove('globalActivities');
$builder->add('globalActivities', APITrueFalseType::class, [
'label' => 'label.globalActivities',
'empty_data' => '1',
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -10,19 +10,7 @@
namespace App\Form\API;
use App\Form\ProjectRateForm;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectRateApiForm extends ProjectRateForm
final class ProjectRateApiForm extends ProjectRateForm
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -10,19 +10,7 @@
namespace App\Form\API;
use App\Form\TagEditForm;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagApiEditForm extends TagEditForm
final class TagApiEditForm extends TagEditForm
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -11,25 +11,12 @@ namespace App\Form\API;
use App\Form\TeamEditForm;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TeamApiEditForm extends TeamEditForm
final class TeamApiEditForm extends TeamEditForm
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
parent::buildForm($builder, $options);
$builder->remove('users');
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -19,25 +19,27 @@ use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimesheetApiEditForm extends TimesheetEditForm
final class TimesheetApiEditForm extends TimesheetEditForm
{
protected function addBillable(FormBuilderInterface $builder, array $options)
protected function addBillable(FormBuilderInterface $builder, array $options): void
{
if (!$options['include_billable']) {
return;
}
$builder->add('billable', BillableType::class, []);
$builder->add('billable', BillableType::class);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) {
$data = $event->getData();
if (\array_key_exists('billable', $data)) {
$data['billableMode'] = Timesheet::BILLABLE_AUTOMATIC;
$event->getForm()->add('billableMode', TimesheetBillableType::class, []);
if ($data['billable'] === true) {
$billable = $data['billable'] === null ? false : (bool) $data['billable'];
if ($billable === true) {
$data['billableMode'] = Timesheet::BILLABLE_YES;
} elseif ($data['billable'] === false) {
} elseif ($billable === false) {
$data['billableMode'] = Timesheet::BILLABLE_NO;
}
}
@@ -46,14 +48,13 @@ class TimesheetApiEditForm extends TimesheetEditForm
);
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
parent::buildForm($builder, $options);
$builder->remove('metaFields');
if ($builder->has('metaFields')) {
$builder->remove('metaFields');
}
if ($builder->has('duration')) {
$builder->remove('duration');
@@ -63,6 +64,11 @@ class TimesheetApiEditForm extends TimesheetEditForm
$builder->get('user')->setRequired(false);
}
// TODO this is only a quick fix, see bugs reports
if ($builder->has('duration')) {
$builder->remove('duration');
}
if ($builder->has('tags')) {
$builder->remove('tags');
// @deprecated for BC reasons here, arrays will be supported in 2.0
@@ -72,7 +78,22 @@ class TimesheetApiEditForm extends TimesheetEditForm
}
}
public function configureOptions(OptionsResolver $resolver)
protected function addBegin(FormBuilderInterface $builder, array $dateTimeOptions, array $options = []): void
{
$builder->add('begin', DateTimeApiType::class, array_merge($dateTimeOptions, [
'label' => 'begin',
]));
}
protected function addEnd(FormBuilderInterface $builder, array $dateTimeOptions, array $options = []): void
{
$builder->add('end', DateTimeApiType::class, array_merge($dateTimeOptions, [
'label' => 'end',
'required' => false,
]));
}
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);

View File

@@ -15,12 +15,9 @@ use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserApiCreateForm extends UserCreateType
final class UserApiCreateForm extends UserCreateType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
parent::buildForm($builder, $options);
@@ -30,7 +27,7 @@ class UserApiCreateForm extends UserCreateType
$builder->add('plainPassword', PasswordType::class, [
'required' => true,
'label' => 'label.password',
'label' => 'password',
'documentation' => [
'type' => 'string',
'description' => 'Plain text password',
@@ -43,7 +40,7 @@ class UserApiCreateForm extends UserCreateType
$builder->add('plainApiToken', PasswordType::class, [
'required' => false,
'label' => 'label.api_token',
'label' => 'api_token',
'documentation' => [
'type' => 'string',
'description' => 'Plain API token',
@@ -52,7 +49,7 @@ class UserApiCreateForm extends UserCreateType
if ($options['include_roles']) {
$builder->add('roles', UserRoleType::class, [
'label' => 'label.roles',
'label' => 'roles',
'required' => false,
'multiple' => true,
'expanded' => false,
@@ -60,14 +57,13 @@ class UserApiCreateForm extends UserCreateType
}
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'csrf_protection' => false,
'include_roles' => true,
'include_add_more' => false,
]);
}
}

View File

@@ -14,18 +14,15 @@ use App\Form\UserEditType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserApiEditForm extends UserEditType
final class UserApiEditForm extends UserEditType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
parent::buildForm($builder, $options);
if ($options['include_roles']) {
$builder->add('roles', UserRoleType::class, [
'label' => 'label.roles',
'label' => 'roles',
'required' => false,
'multiple' => true,
'expanded' => false,
@@ -33,7 +30,7 @@ class UserApiEditForm extends UserEditType
}
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
parent::configureOptions($resolver);

View File

@@ -29,7 +29,7 @@ abstract class AbstractRateForm extends AbstractType
'type' => 'number',
'description' => 'The rate (eg. 10.5)',
],
'label' => 'label.rate',
'label' => 'rate',
'attr' => [
'autofocus' => 'autofocus'
],
@@ -42,13 +42,13 @@ abstract class AbstractRateForm extends AbstractType
'type' => 'number',
'description' => 'The internal rate (eg. 10.0 or 10)',
],
'label' => 'label.rate_internal',
'label' => 'internalRate',
'currency' => $currency,
'required' => false,
'help' => 'help.rate_internal',
'help' => 'help.internalRate',
])
->add('isFixed', YesNoType::class, [
'label' => 'label.fixedRate',
'label' => 'fixedRate',
'help' => 'help.fixedRate',
'documentation' => [
'type' => 'boolean',

View File

@@ -11,7 +11,9 @@ namespace App\Form;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Form\Type\InvoiceLabelType;
use App\Form\Type\ProjectType;
use App\Form\Type\TeamType;
use App\Repository\ProjectRepository;
use App\Repository\Query\ProjectFormTypeQuery;
use Symfony\Component\Form\AbstractType;
@@ -24,78 +26,79 @@ class ActivityEditForm extends AbstractType
{
use EntityFormTrait;
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$project = null;
$customer = null;
$new = true;
$isNew = true;
$isGlobal = false;
$options['currency'] = null;
if (isset($options['data'])) {
/** @var Activity $entry */
$entry = $options['data'];
$isGlobal = $entry->isGlobal();
if (null !== $entry->getProject()) {
if (!$isGlobal) {
$project = $entry->getProject();
$customer = $project->getCustomer();
$options['currency'] = $customer->getCurrency();
} else {
$isGlobal = null === $entry->getProject();
}
$new = $entry->getId() === null;
$isNew = $entry->getId() === null;
}
$builder
->add('name', TextType::class, [
'label' => 'label.name',
'label' => 'name',
'attr' => [
'autofocus' => 'autofocus'
],
])
->add('comment', TextareaType::class, [
'label' => 'label.description',
'required' => false,
])
->add('invoiceText', TextareaType::class, [
'label' => 'label.invoiceText',
'label' => 'description',
'required' => false,
])
->add('invoiceText', InvoiceLabelType::class)
;
if ($new || !$isGlobal) {
if ($isNew || !$isGlobal) {
$builder
->add('project', ProjectType::class, [
'required' => false,
'help' => 'help.globalActivity',
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer) {
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
$query->setIgnoreDate(true);
$query->setWithCustomer(true);
return $repo->getQueryBuilderForFormType($query);
},
]);
}
if ($isNew) {
$builder
->add('teams', TeamType::class, [
'required' => false,
'multiple' => true,
'expanded' => false,
'by_reference' => false,
'help' => 'help.teams',
]);
}
$this->addCommonFields($builder, $options);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Activity::class,
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'admin_activity_edit',
// @deprecated not supported since 1.15, which removed the customer select completely
'customer' => false,
'currency' => Customer::DEFAULT_CURRENCY,
'include_budget' => false,
'include_time' => false,

View File

@@ -15,10 +15,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class ActivityRateForm extends AbstractRateForm
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$currency = null;
@@ -34,10 +31,7 @@ class ActivityRateForm extends AbstractRateForm
$this->addFields($builder, $currency);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ActivityRate::class,

View File

@@ -15,12 +15,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ActivityTeamPermissionForm extends AbstractType
final class ActivityTeamPermissionForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('teams', TeamType::class, [
@@ -31,10 +28,7 @@ class ActivityTeamPermissionForm extends AbstractType
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Activity::class,

View File

@@ -14,7 +14,7 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CalendarForm extends AbstractType
final class CalendarForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{

View File

@@ -15,24 +15,16 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerCommentForm extends AbstractType
final class CustomerCommentForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('message', TextareaType::class, [
'label' => false,
])
;
$builder->add('message', TextareaType::class, [
'label' => false,
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => CustomerComment::class,

View File

@@ -10,7 +10,9 @@
namespace App\Form;
use App\Entity\Customer;
use App\Form\Type\InvoiceTemplateType;
use App\Form\Type\MailType;
use App\Form\Type\TeamType;
use App\Form\Type\TimezoneType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
@@ -26,86 +28,108 @@ class CustomerEditForm extends AbstractType
{
use EntityFormTrait;
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$isNew = false;
if (isset($options['data'])) {
/** @var Customer $customer */
$customer = $options['data'];
$isNew = $customer->getId() === null;
$options['currency'] = $customer->getCurrency();
}
$builder
->add('name', TextType::class, [
'label' => 'label.name',
'label' => 'name',
'attr' => [
'autofocus' => 'autofocus'
],
])
->add('number', TextType::class, [
'label' => 'label.number',
'label' => 'number',
'required' => false,
])
->add('comment', TextareaType::class, [
'label' => 'label.description',
'label' => 'description',
'required' => false,
])
->add('company', TextType::class, [
'label' => 'label.company',
'label' => 'company',
'required' => false,
])
->add('vatId', TextType::class, [
'label' => 'label.vat_id',
'label' => 'vat_id',
'required' => false,
])
->add('contact', TextType::class, [
'label' => 'label.contact',
'label' => 'contact',
'required' => false,
])
->add('address', TextareaType::class, [
'label' => 'label.address',
'label' => 'address',
'required' => false,
])
->add('country', CountryType::class, [
'label' => 'label.country',
'label' => 'country',
])
->add('currency', CurrencyType::class, [
'label' => 'label.currency',
'label' => 'currency',
])
->add('phone', TelType::class, [
'label' => 'label.phone',
'label' => 'phone',
'required' => false,
'block_prefix' => 'phone',
])
->add('fax', TelType::class, [
'label' => 'label.fax',
'label' => 'fax',
'required' => false,
'attr' => ['icon' => 'fax'],
'block_prefix' => 'phone',
])
->add('mobile', TelType::class, [
'label' => 'label.mobile',
'label' => 'mobile',
'required' => false,
'attr' => ['icon' => 'mobile'],
'block_prefix' => 'phone',
])
->add('email', MailType::class, [
'required' => false,
])
->add('homepage', UrlType::class, [
'label' => 'label.homepage',
'label' => 'homepage',
'required' => false,
'block_prefix' => 'homepage',
])
->add('timezone', TimezoneType::class, [
'label' => 'label.timezone',
]);
'label' => 'timezone',
])
->add('invoiceText', TextareaType::class, [
'label' => 'invoiceText',
'help' => 'help.invoiceText',
'required' => false,
])
->add('invoiceTemplate', InvoiceTemplateType::class, [
'help' => 'help.invoiceTemplate_customer',
'required' => false,
])
;
if ($isNew) {
$builder
->add('teams', TeamType::class, [
'required' => false,
'multiple' => true,
'expanded' => false,
'by_reference' => false,
'help' => 'help.teams',
]);
}
$this->addCommonFields($builder, $options);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Customer::class,

View File

@@ -15,10 +15,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerRateForm extends AbstractRateForm
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$currency = null;
@@ -34,10 +31,7 @@ class CustomerRateForm extends AbstractRateForm
$this->addFields($builder, $currency);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => CustomerRate::class,

View File

@@ -15,26 +15,18 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CustomerTeamPermissionForm extends AbstractType
final class CustomerTeamPermissionForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('teams', TeamType::class, [
'multiple' => true,
'expanded' => true,
'by_reference' => false,
])
;
$builder->add('teams', TeamType::class, [
'multiple' => true,
'expanded' => true,
'by_reference' => false,
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Customer::class,

View File

@@ -14,32 +14,16 @@ use App\Validator\Constraints\Duration as DurationConstraint;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class DurationStringToSecondsTransformer implements DataTransformerInterface
final class DurationStringToSecondsTransformer implements DataTransformerInterface
{
/**
* @var Duration
*/
protected $formatter;
/**
* @var string
*/
private $pattern;
public function __construct()
{
$this->formatter = new Duration();
$constraint = new DurationConstraint();
$this->pattern = $constraint->pattern;
}
/**
* @param int $intToFormat
* @return string|null
*/
public function transform($intToFormat)
public function transform(mixed $intToFormat): ?string
{
try {
return $this->formatter->format($intToFormat);
return (new Duration())->format($intToFormat);
} catch (\Exception | \TypeError $e) {
throw new TransformationFailedException($e->getMessage());
}
@@ -49,7 +33,7 @@ class DurationStringToSecondsTransformer implements DataTransformerInterface
* @param string|null $formatToInt
* @return int|null
*/
public function reverseTransform($formatToInt)
public function reverseTransform(mixed $formatToInt): ?int
{
if (null === $formatToInt) {
return null;
@@ -60,12 +44,12 @@ class DurationStringToSecondsTransformer implements DataTransformerInterface
}
// we need this one here, because the data transformer is executed BEFORE the constraint is called
if (!preg_match($this->pattern, $formatToInt)) {
if (!preg_match((new DurationConstraint())->pattern, $formatToInt)) {
throw new TransformationFailedException('Invalid duration format given');
}
try {
$seconds = $this->formatter->parseDurationString($formatToInt);
$seconds = (new Duration())->parseDurationString($formatToInt);
// DateTime throws if a duration with too many seconds is passed and an amount of so
// many seconds is likely not required in a time-tracking application ;-)

View File

@@ -13,7 +13,7 @@ use App\Utils\SearchTerm;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class SearchTermTransformer implements DataTransformerInterface
final class SearchTermTransformer implements DataTransformerInterface
{
/**
* Transforms a SearchTerm object to a string.
@@ -21,7 +21,7 @@ class SearchTermTransformer implements DataTransformerInterface
* @param SearchTerm|null $searchTerm
* @return string
*/
public function transform($searchTerm)
public function transform(mixed $searchTerm): mixed
{
if (empty($searchTerm) || !($searchTerm instanceof SearchTerm)) {
return '';
@@ -37,7 +37,7 @@ class SearchTermTransformer implements DataTransformerInterface
* @return SearchTerm|null
* @throws TransformationFailedException if object (issue) is not found
*/
public function reverseTransform($searchTerm)
public function reverseTransform(mixed $searchTerm): mixed
{
if (empty($searchTerm)) {
return null;

View File

@@ -0,0 +1,58 @@
<?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\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @implements DataTransformerInterface<array, string>
*/
final class StringToArrayTransformer implements DataTransformerInterface
{
/**
* @param non-empty-string $separator
*/
public function __construct(private readonly string $separator = ',')
{
}
/**
* Transforms an array of strings to a string.
*
* @param array<string> $value
* @return string
*/
public function transform(mixed $value): mixed
{
if (empty($value)) {
return '';
}
return implode($this->separator, $value);
}
/**
* Transforms a string to an array of tags.
*
* @param string|null $value
* @return array<string>
* @throws TransformationFailedException
*/
public function reverseTransform(mixed $value): mixed
{
// check for empty list
if ('' === $value || null === $value) {
return [];
}
return array_filter(array_unique(array_map('trim', explode($this->separator, $value))));
}
}

View File

@@ -14,16 +14,10 @@ use App\Repository\TagRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class TagArrayToStringTransformer implements DataTransformerInterface
final class TagArrayToStringTransformer implements DataTransformerInterface
{
/**
* @var TagRepository
*/
private $tagRepository;
public function __construct(TagRepository $tagRepository)
public function __construct(private TagRepository $tagRepository)
{
$this->tagRepository = $tagRepository;
}
/**
@@ -32,7 +26,7 @@ class TagArrayToStringTransformer implements DataTransformerInterface
* @param Tag[]|null $tags
* @return string
*/
public function transform($tags)
public function transform(mixed $tags): mixed
{
if (empty($tags)) {
return '';
@@ -50,7 +44,7 @@ class TagArrayToStringTransformer implements DataTransformerInterface
* @return Tag[]
* @throws TransformationFailedException
*/
public function reverseTransform($stringOfTags)
public function reverseTransform(mixed $stringOfTags): mixed
{
// check for empty tag list
if ('' === $stringOfTags || null === $stringOfTags) {

View File

@@ -14,7 +14,6 @@ use App\Form\Type\BudgetType;
use App\Form\Type\DurationType;
use App\Form\Type\MetaFieldsCollectionType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -33,7 +32,7 @@ trait EntityFormTrait
if ($showMoney) {
$builder->add('budget', MoneyType::class, [
'empty_data' => '0.00',
'label' => 'label.budget',
'label' => 'budget',
'required' => false,
'currency' => $options['currency'],
]);
@@ -42,7 +41,7 @@ trait EntityFormTrait
if ($showTime) {
$builder->add('timeBudget', DurationType::class, [
'empty_data' => 0,
'label' => 'label.timeBudget',
'label' => 'timeBudget',
'icon' => 'clock',
'required' => false,
]);
@@ -56,22 +55,10 @@ trait EntityFormTrait
$builder
->add('visible', YesNoType::class, [
'label' => 'label.visible',
'label' => 'visible',
'help' => 'help.visible',
])
->add('billable', BillableType::class)
;
}
/**
* @deprecated since 1.15
* @param FormBuilderInterface $builder
*/
public function addCreateMore(FormBuilderInterface $builder): void
{
$builder->add('create_more', CheckboxType::class, [
'label' => 'label.create_more',
'required' => false,
'mapped' => false,
]);
}
}

View File

@@ -0,0 +1,82 @@
<?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\Extension;
use App\Form\API\ActivityApiEditForm;
use App\Form\API\ActivityRateApiForm;
use App\Form\API\CustomerApiEditForm;
use App\Form\API\CustomerRateApiForm;
use App\Form\API\ProjectApiEditForm;
use App\Form\API\ProjectRateApiForm;
use App\Form\API\TagApiEditForm;
use App\Form\API\TeamApiEditForm;
use App\Form\API\TimesheetApiEditForm;
use App\Form\API\UserApiCreateForm;
use App\Form\API\UserApiEditForm;
use App\Form\Type\BillableType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ApiFormExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [
ActivityApiEditForm::class,
ActivityRateApiForm::class,
CustomerApiEditForm::class,
CustomerRateApiForm::class,
ProjectApiEditForm::class,
ProjectRateApiForm::class,
TagApiEditForm::class,
TeamApiEditForm::class,
TimesheetApiEditForm::class,
UserApiCreateForm::class,
UserApiEditForm::class,
];
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if ($builder->has('metaFields')) {
$builder->remove('metaFields');
}
$replaces = [];
foreach ($builder as $child) {
if (\in_array($child->getType()->getInnerType()->getParent(), [CheckboxType::class, BillableType::class, YesNoType::class])) {
$replaces[] = [
'name' => $child->getName(),
'label' => $child->getFormConfig()->getOption('label'),
'required' => $child->getFormConfig()->getRequired(),
];
}
}
foreach ($replaces as $field) {
$builder->remove($field['name']);
$builder->add($field['name'], CheckboxType::class, [
'label' => $field['label'],
'required' => $field['required'],
]);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}

View File

@@ -18,7 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Adds a linked help text with the link to the given documentation
*/
class DocumentationLinkExtension extends AbstractTypeExtension
final class DocumentationLinkExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
@@ -30,7 +30,7 @@ class DocumentationLinkExtension extends AbstractTypeExtension
* @param FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['docu_chapter'] = $options['docu_chapter'] ?? null;
}
@@ -38,7 +38,7 @@ class DocumentationLinkExtension extends AbstractTypeExtension
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['docu_chapter']);
$resolver->setAllowedTypes('docu_chapter', 'string');

View File

@@ -21,11 +21,6 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
{
/**
* @deprecated since 1.7 will be removed with 2.0
*/
public const TYPE_SELECTPICKER = 'selectpicker';
public static function getExtendedTypes(): iterable
{
return [EntityType::class, ChoiceType::class];
@@ -36,7 +31,7 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
* @param FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
if (isset($options['selectpicker']) && false === $options['selectpicker']) {
return;
@@ -59,16 +54,13 @@ final class EnhancedChoiceTypeExtension extends AbstractTypeExtension
}
if (false === $options['search']) {
$extendedOptions['data-minimum-results-for-search'] = 'Infinity';
$extendedOptions['data-disable-search'] = 1;
}
$view->vars['attr'] = array_merge($view->vars['attr'], $extendedOptions);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['selectpicker']);
$resolver->setAllowedTypes('selectpicker', 'boolean');

View File

@@ -18,7 +18,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Allows to register custom icons at form fields
*/
class IconExtension extends AbstractTypeExtension
final class IconExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
@@ -30,7 +30,7 @@ class IconExtension extends AbstractTypeExtension
* @param FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['icon'] = $options['icon'] ?? null;
}
@@ -38,7 +38,7 @@ class IconExtension extends AbstractTypeExtension
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['icon']);
$resolver->setAllowedTypes('icon', 'string');

View File

@@ -19,19 +19,10 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Support Remote-API calls for Entity select-boxes.
*/
class SelectWithApiDataExtension extends AbstractTypeExtension
final class SelectWithApiDataExtension extends AbstractTypeExtension
{
/**
* @var UrlGeneratorInterface
*/
private $router;
/**
* @param UrlGeneratorInterface $router
*/
public function __construct(UrlGeneratorInterface $router)
public function __construct(private UrlGeneratorInterface $router)
{
$this->router = $router;
}
public static function getExtendedTypes(): iterable
@@ -44,7 +35,7 @@ class SelectWithApiDataExtension extends AbstractTypeExtension
* @param FormInterface $form
* @param array $options
*/
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
if (!isset($options['api_data'])) {
return;
@@ -56,6 +47,12 @@ class SelectWithApiDataExtension extends AbstractTypeExtension
throw new \InvalidArgumentException('Option "api_data" must be an array for form "' . $form->getName() . '"');
}
if (isset($apiData['create'])) {
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-create' => $this->router->generate($apiData['create']),
]);
}
if (!isset($apiData['select'])) {
return;
}
@@ -93,12 +90,15 @@ class SelectWithApiDataExtension extends AbstractTypeExtension
'data-empty-url' => $this->router->generate($apiData['route'], $apiData['empty_route_params']),
]);
}
if (isset($apiData['reload'])) {
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-reload' => $this->router->generate($apiData['reload']),
]);
}
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['api_data']);
$resolver->setAllowedTypes('api_data', 'array');

View File

@@ -12,30 +12,25 @@ namespace App\Form\Extension;
use App\Form\Helper\ToolbarHelper;
use App\Form\Toolbar\ExportToolbarForm;
use App\Form\Toolbar\InvoiceToolbarForm;
use App\Form\Toolbar\InvoiceToolbarSimpleForm;
use App\Form\Toolbar\TimesheetExportToolbarForm;
use App\Form\Toolbar\TimesheetToolbarForm;
use App\Form\Toolbar\UserToolbarForm;
use App\Reporting\MonthlyUserListForm;
use App\Reporting\WeeklyUserListForm;
use App\Reporting\YearlyUserListForm;
use App\Reporting\MonthlyUserList\MonthlyUserListForm;
use App\Reporting\WeeklyUserList\WeeklyUserListForm;
use App\Reporting\YearlyUserList\YearlyUserListForm;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormBuilderInterface;
final class ToolbarFormExtension extends AbstractTypeExtension
{
private $toolbarHelper;
public function __construct(ToolbarHelper $toolbarHelper)
public function __construct(private ToolbarHelper $toolbarHelper)
{
$this->toolbarHelper = $toolbarHelper;
}
public static function getExtendedTypes(): iterable
{
return [
InvoiceToolbarForm::class,
InvoiceToolbarSimpleForm::class,
ExportToolbarForm::class,
TimesheetToolbarForm::class,
TimesheetExportToolbarForm::class,
@@ -46,7 +41,7 @@ final class ToolbarFormExtension extends AbstractTypeExtension
];
}
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->toolbarHelper->cleanupForm($builder);
}

View File

@@ -17,14 +17,8 @@ use Symfony\Component\Security\Core\Security;
final class UserExtension extends AbstractTypeExtension
{
/**
* @var Security
*/
private $security;
public function __construct(Security $security)
public function __construct(private Security $security)
{
$this->security = $security;
}
public static function getExtendedTypes(): iterable
@@ -32,7 +26,7 @@ final class UserExtension extends AbstractTypeExtension
return [FormType::class];
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['user']);
// null needs to be allowed, as there is no user for anonymous forms (like "forgot password" and "registration")

View File

@@ -14,9 +14,7 @@ use App\Entity\Customer;
use App\Entity\Project;
use App\Form\Type\ActivityType;
use App\Form\Type\CustomerType;
use App\Form\Type\DescriptionType;
use App\Form\Type\ProjectType;
use App\Form\Type\TagsType;
use App\Repository\ProjectRepository;
use App\Repository\Query\ProjectFormTypeQuery;
use Symfony\Component\Form\FormBuilderInterface;
@@ -30,12 +28,12 @@ use Symfony\Component\Form\FormEvents;
*/
trait FormTrait
{
protected function addCustomer(FormBuilderInterface $builder, ?Customer $customer = null)
protected function addCustomer(FormBuilderInterface $builder, ?Customer $customer = null): void
{
$builder->add('customer', CustomerType::class, [
'query_builder_for_user' => true,
'customers' => $customer,
'data' => $customer ? $customer : '',
'data' => $customer,
'required' => false,
'placeholder' => '',
'mapped' => false,
@@ -43,7 +41,7 @@ trait FormTrait
]);
}
protected function addProject(FormBuilderInterface $builder, bool $isNew, ?Project $project = null, ?Customer $customer = null, array $options = [])
protected function addProject(FormBuilderInterface $builder, bool $isNew, ?Project $project = null, ?Customer $customer = null, array $options = []): void
{
$options = array_merge([
'placeholder' => '',
@@ -61,26 +59,39 @@ trait FormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options) {
/** @var array<string, mixed> $data */
$data = $event->getData();
$customer = isset($data['customer']) && !empty($data['customer']) ? $data['customer'] : null;
$project = isset($data['project']) && !empty($data['project']) ? $data['project'] : $project;
$customer = \array_key_exists('customer', $data) && $data['customer'] !== '' ? $data['customer'] : null;
$project = \array_key_exists('project', $data) && $data['project'] !== '' ? $data['project'] : $project;
$event->getForm()->add('project', ProjectType::class, array_merge($options, [
'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
// is there a better wa to prevent starting a record with a hidden project ?
if ($isNew && !empty($project) && (\is_int($project) || \is_string($project))) {
// is there a better way to prevent starting a record with a hidden project ?
$project = \is_string($project) ? (int) $project : $project;
$customer = \is_string($customer) ? (int) $customer : $customer;
if ($isNew && \is_int($project)) {
/** @var Project $project */
$project = $repo->find($project);
if (null !== $project) {
if (!$project->getCustomer()->isVisible()) {
$customer = null;
$project = null;
} elseif (!$project->isVisible()) {
$project = null;
}
if ($project === null) {
throw new \Exception('Unknown project');
}
if (!$project->getCustomer()->isVisible()) {
$customer = null;
$project = null;
} elseif (!$project->isVisible()) {
$project = null;
}
}
if ($project !== null && !\is_int($project) && !($project instanceof Project)) {
throw new \InvalidArgumentException('Project type needs a project object or an ID');
}
if ($customer !== null && !\is_int($customer) && !($customer instanceof Customer)) {
throw new \InvalidArgumentException('Project type needs a customer object or an ID');
}
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
$query->setWithCustomer(true);
@@ -92,7 +103,7 @@ trait FormTrait
);
}
protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null, array $options = [])
protected function addActivity(FormBuilderInterface $builder, ?Activity $activity = null, ?Project $project = null, array $options = []): void
{
$options = array_merge(['placeholder' => '', 'query_builder_for_user' => true], $options);
@@ -105,8 +116,10 @@ trait FormTrait
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($options) {
/** @var array<string, mixed> $data */
$data = $event->getData();
if (!isset($data['project']) || empty($data['project'])) {
if (!\array_key_exists('project', $data) || $data['project'] === '' || $data['project'] === null) {
return;
}
@@ -116,31 +129,4 @@ trait FormTrait
}
);
}
/**
* @deprecated since 1.13
*/
protected function addDescription(FormBuilderInterface $builder)
{
@trigger_error('FormTrait::addDescription() is deprecated and will be removed with 2.0, use DescriptionType instead', E_USER_DEPRECATED);
$builder->add('description', DescriptionType::class, [
'required' => false,
'attr' => [
'autofocus' => 'autofocus'
]
]);
}
/**
* @deprecated since 1.14
*/
protected function addTags(FormBuilderInterface $builder)
{
@trigger_error('FormTrait::addTags() is deprecated and will be removed with 2.0, use TagsType instead', E_USER_DEPRECATED);
$builder->add('tags', TagsType::class, [
'required' => false,
]);
}
}

View File

@@ -19,12 +19,10 @@ final class ActivityHelper
public const PATTERN_SPACER = '{spacer}';
public const SPACER = ' - ';
private $configuration;
private $pattern;
private ?string $pattern = null;
public function __construct(SystemConfiguration $configuration)
public function __construct(private SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
public function getChoicePattern(): string

View File

@@ -21,12 +21,10 @@ final class CustomerHelper
public const PATTERN_SPACER = '{spacer}';
public const SPACER = ' - ';
private $configuration;
private $pattern;
private ?string $pattern = null;
public function __construct(SystemConfiguration $configuration)
public function __construct(private SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
public function getChoicePattern(): string

View File

@@ -9,9 +9,10 @@
namespace App\Form\Helper;
use App\Configuration\LocaleService;
use App\Configuration\SystemConfiguration;
use App\Entity\Project;
use App\Utils\LocaleSettings;
use Symfony\Contracts\Translation\TranslatorInterface;
final class ProjectHelper
{
@@ -25,15 +26,13 @@ final class ProjectHelper
public const PATTERN_SPACER = '{spacer}';
public const SPACER = ' - ';
private $configuration;
private $localeSettings;
private $dateFormat;
private $pattern;
private ?\IntlDateFormatter $dateFormatter = null;
private ?string $pattern = null;
private bool $showStart = false;
private bool $showEnd = false;
public function __construct(SystemConfiguration $configuration, LocaleSettings $localeSettings)
public function __construct(private SystemConfiguration $configuration, private LocaleService $localeService, private TranslatorInterface $translator)
{
$this->configuration = $configuration;
$this->localeSettings = $localeSettings;
}
public function getChoicePattern(): string
@@ -54,27 +53,40 @@ final class ProjectHelper
public function getChoiceLabel(Project $project): string
{
if ($this->dateFormat === null) {
$this->dateFormat = $this->localeSettings->getDateFormat();
}
$start = '?';
if ($project->getStart() !== null) {
$start = $project->getStart()->format($this->dateFormat);
}
$end = '?';
if ($project->getEnd() !== null) {
$end = $project->getEnd()->format($this->dateFormat);
}
$name = $this->getChoicePattern();
$name = str_replace(self::PATTERN_NAME, $project->getName(), $name);
$name = str_replace(self::PATTERN_COMMENT, $project->getComment() ?? '', $name);
$name = str_replace(self::PATTERN_CUSTOMER, $project->getCustomer()->getName() ?? '', $name);
$name = str_replace(self::PATTERN_ORDERNUMBER, $project->getOrderNumber() ?? '', $name);
$name = str_replace(self::PATTERN_START, $start, $name);
$name = str_replace(self::PATTERN_END, $end, $name);
if ($this->dateFormatter === null) {
$this->showStart = stripos($name, self::PATTERN_START) !== false;
$this->showEnd = stripos($name, self::PATTERN_END) !== false;
$this->dateFormatter = new \IntlDateFormatter(
\Locale::getDefault(),
\IntlDateFormatter::MEDIUM,
\IntlDateFormatter::MEDIUM,
date_default_timezone_get(),
\IntlDateFormatter::GREGORIAN,
$this->localeService->getDateFormat(\Locale::getDefault())
);
}
if ($this->showStart) {
$start = '';
if ($project->getStart() !== null) {
$start = $this->translator->trans('project_start') . ': ' . $this->dateFormatter->format($project->getStart()) . ' ';
}
$name = str_replace(self::PATTERN_START, $start, $name);
}
if ($this->showEnd) {
$end = '';
if ($project->getEnd() !== null) {
$end = ' ' . $this->translator->trans('project_end') . ': ' . $this->dateFormatter->format($project->getEnd());
}
$name = str_replace(self::PATTERN_END, $end, $name);
}
$name = ltrim($name, self::SPACER);
$name = rtrim($name, self::SPACER);

View File

@@ -15,18 +15,16 @@ use Symfony\Component\Form\FormBuilderInterface;
final class ToolbarHelper
{
private $userService;
private $teamService;
private $teamNames = ['team', 'teams', 'searchTeams'];
private $userNames = ['user', 'users'];
/** @var array<string> */
private array $teamNames = ['team', 'teams', 'searchTeams'];
/** @var array<string> */
private array $userNames = ['user', 'users'];
public function __construct(UserService $userService, TeamService $teamService)
public function __construct(private UserService $userService, private TeamService $teamService)
{
$this->userService = $userService;
$this->teamService = $teamService;
}
public function cleanupForm(FormBuilderInterface $builder)
public function cleanupForm(FormBuilderInterface $builder): void
{
$deleteUser = false;
foreach ($this->userNames as $name) {

View File

@@ -19,19 +19,13 @@ use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class InvoiceDocumentUploadForm extends AbstractType
final class InvoiceDocumentUploadForm extends AbstractType
{
private $repository;
public function __construct(InvoiceDocumentRepository $repository)
public function __construct(private InvoiceDocumentRepository $repository)
{
$this->repository = $repository;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$mimetypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -41,7 +35,7 @@ class InvoiceDocumentUploadForm extends AbstractType
$builder
->add('document', FileType::class, [
'label' => 'label.invoice_renderer',
'label' => 'invoice_renderer',
'translation_domain' => 'invoice-renderer',
'help' => 'help.upload',
'mapped' => false,
@@ -80,10 +74,7 @@ class InvoiceDocumentUploadForm extends AbstractType
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'csrf_protection' => true,

View File

@@ -18,12 +18,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class InvoiceEditForm extends AbstractType
final class InvoiceEditForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$dateTimeOptions = [
'model_timezone' => $options['timezone'],
@@ -32,7 +29,7 @@ class InvoiceEditForm extends AbstractType
$builder
->add('comment', TextareaType::class, [
'label' => 'label.description',
'label' => 'description',
'required' => false,
])
->add('status', ChoiceType::class, [
@@ -42,7 +39,7 @@ class InvoiceEditForm extends AbstractType
'status.paid' => Invoice::STATUS_PAID,
'status.canceled' => Invoice::STATUS_CANCELED,
],
'label' => 'label.status',
'label' => 'status',
'required' => true,
])
->add('paymentDate', DatePickerType::class, array_merge($dateTimeOptions, [
@@ -53,10 +50,7 @@ class InvoiceEditForm extends AbstractType
$builder->add('metaFields', MetaFieldsCollectionType::class);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Invoice::class,

View File

@@ -14,7 +14,6 @@ use App\Form\Type\InvoiceCalculatorType;
use App\Form\Type\InvoiceNumberGeneratorType;
use App\Form\Type\InvoiceRendererType;
use App\Form\Type\LanguageType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
@@ -26,66 +25,55 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to manipulate invoice templates.
*/
class InvoiceTemplateForm extends AbstractType
final class InvoiceTemplateForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'label.name',
'label' => 'name',
])
->add('title', TextType::class, [
'label' => 'label.title',
'label' => 'title',
])
->add('company', TextType::class, [
'label' => 'label.company',
'label' => 'company',
])
->add('vatId', TextType::class, [
'label' => 'label.vat_id',
'label' => 'vat_id',
'required' => false,
])
->add('address', TextareaType::class, [
'label' => 'label.address',
'label' => 'address',
'required' => false,
])
->add('contact', TextareaType::class, [
'label' => 'label.contact',
'label' => 'contact',
'required' => false,
])
->add('paymentTerms', TextareaType::class, [
'label' => 'label.payment_terms',
'label' => 'payment_terms',
'required' => false,
])
->add('paymentDetails', TextareaType::class, [
'label' => 'label.invoice_bank_account',
'label' => 'invoice_bank_account',
'required' => false,
])
->add('dueDays', IntegerType::class, [
'label' => 'label.due_days',
'label' => 'due_days',
])
->add('vat', NumberType::class, [
'label' => 'label.tax_rate',
'label' => 'tax_rate',
'scale' => 3,
])
->add('renderer', InvoiceRendererType::class)
->add('calculator', InvoiceCalculatorType::class)
->add('numberGenerator', InvoiceNumberGeneratorType::class)
->add('language', LanguageType::class, [
'required' => true,
])
->add('decimalDuration', YesNoType::class, [
'label' => 'label.decimalDuration'
])
->add('language', LanguageType::class)
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => InvoiceTemplate::class,
@@ -97,6 +85,7 @@ class InvoiceTemplateForm extends AbstractType
'data-msg-success' => 'action.update.success',
'data-msg-error' => 'action.update.error',
],
'docu_chapter' => 'invoices.html',
]);
}
}

View File

@@ -9,73 +9,44 @@
namespace App\Form\Model;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Validator\Constraint;
final class Configuration
{
/**
* @var string
*/
private $name;
/**
* @var string|null
*/
private $label;
/**
* @var string
*/
private $translationDomain = 'messages';
/**
* @var mixed
*/
private $value;
/**
* @var string
*/
private $type;
/**
* @var array
*/
private $options = [];
/**
* @var bool
*/
private $enabled = true;
/**
* @var bool
*/
private $required = true;
private ?string $label = null;
private string $translationDomain = 'messages';
private string|int|null|bool|array $value = null;
private ?string $type = null;
private array $options = [];
private bool $enabled = true;
private bool $required = true;
/**
* @var Constraint[]
*/
private $constraints = [];
private array $constraints = [];
public function getName(): ?string
public function __construct(private string $name)
{
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): Configuration
{
$this->name = $name;
return $this;
}
/**
* @return mixed
*/
public function getValue()
public function getValue(): string|int|null|bool|object
{
return $this->value;
}
/**
* @param mixed $value
* @return Configuration
*/
public function setValue($value): Configuration
public function setValue(string|int|null|bool|object $value): Configuration
{
if ($this->type === CheckboxType::class || $this->type === YesNoType::class) {
$value = (bool) $value;
}
$this->value = $value;
return $this;

View File

@@ -13,13 +13,11 @@ use DateTime;
final class DateRange
{
private $resetTimes;
private $begin;
private $end;
private ?DateTime $begin = null;
private ?DateTime $end = null;
public function __construct(bool $resetTimes = true)
public function __construct(private bool $resetTimes = true)
{
$this->resetTimes = $resetTimes;
}
public function getBegin(): ?DateTime

View File

@@ -12,22 +12,21 @@ namespace App\Form\Model;
use App\Entity\Team;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* @App\Validator\Constraints\TimesheetMultiUser
*/
#[Constraints\TimesheetMultiUser]
final class MultiUserTimesheet extends Timesheet
{
/**
* @var Collection<User>
*/
private $users;
private Collection $users;
/**
* @var Collection<Team>
*/
private $teams;
private Collection $teams;
public function __construct()
{
@@ -44,20 +43,16 @@ final class MultiUserTimesheet extends Timesheet
return $this->users;
}
public function addUser(User $user)
public function addUser(User $user): void
{
$this->users->add($user);
return $this;
}
public function removeUser(User $user)
public function removeUser(User $user): void
{
if ($this->users->contains($user)) {
$this->users->remove($user);
}
return $this;
}
/**
@@ -68,19 +63,15 @@ final class MultiUserTimesheet extends Timesheet
return $this->teams;
}
public function addTeam(Team $team)
public function addTeam(Team $team): void
{
$this->teams->add($team);
return $this;
}
public function removeTeam(Team $team)
public function removeTeam(Team $team): void
{
if ($this->teams->contains($team)) {
$this->teams->remove($team);
}
return $this;
}
}

View File

@@ -11,47 +11,15 @@ namespace App\Form\Model;
final class SystemConfiguration
{
/** @deprecated since 1.16.10 */
public const SECTION_ROUNDING = 'rounding';
/** @deprecated since 1.16.10 */
public const SECTION_LOCKDOWN = 'lockdown_period';
/** @deprecated since 1.16.10 */
public const SECTION_TIMESHEET = 'timesheet';
/** @deprecated since 1.16.10 */
public const SECTION_FORM_INVOICE = 'invoice';
/** @deprecated since 1.16.10 */
public const SECTION_FORM_CUSTOMER = 'customer';
/** @deprecated since 1.16.10 */
public const SECTION_FORM_USER = 'user';
/** @deprecated since 1.16.10 */
public const SECTION_THEME = 'theme';
/** @deprecated since 1.16.10 */
public const SECTION_AUTHENTICATION = 'authentication';
/** @deprecated since 1.16.10 */
public const SECTION_CALENDAR = 'calendar';
/** @deprecated since 1.16.10 */
public const SECTION_BRANDING = 'branding';
/**
* @var string|null
*/
private $section;
/**
* @var string|null
*/
private $translation;
/**
* @var string|null
*/
private $translationDomain = 'system-configuration';
private ?string $translation = null;
private string $translationDomain = 'system-configuration';
/**
* @var Configuration[]
*/
private $configuration = [];
private array $configuration = [];
public function __construct(?string $section = null)
public function __construct(private ?string $section = null)
{
$this->section = $section;
}
public function getSection(): ?string
@@ -59,18 +27,6 @@ final class SystemConfiguration
return $this->section;
}
/**
* @deprecated since 1.16.10
* @param string|null $section
* @return $this
*/
public function setSection(?string $section): SystemConfiguration
{
$this->section = $section;
return $this;
}
public function setTranslation(string $translation): SystemConfiguration
{
$this->translation = $translation;

View File

@@ -0,0 +1,36 @@
<?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\Model;
use App\Entity\User;
final class TotpActivation
{
private ?string $code = null;
public function __construct(private User $user)
{
}
public function getUser(): User
{
return $this->user;
}
public function setCode(?string $code): void
{
$this->code = $code;
}
public function getCode(): ?string
{
return $this->code;
}
}

View File

@@ -13,17 +13,14 @@ use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\ButtonType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MultiUpdateTable extends AbstractType
final class MultiUpdateTable extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
/** @var EntityRepository $repository */
$repository = $options['repository'];
@@ -36,7 +33,7 @@ class MultiUpdateTable extends AbstractType
$builder->get('entities')->addModelTransformer(
new CallbackTransformer(
function ($ids) {
function ($ids): string {
return implode(',', $ids);
},
function ($ids) use ($repository) {
@@ -49,18 +46,22 @@ class MultiUpdateTable extends AbstractType
)
);
$builder->add('action', ChoiceType::class, [
'mapped' => false,
'required' => false,
'choices' => $dto->getActions(),
'search' => false,
]);
$i = 0;
foreach ($dto->getActions() as $key => $value) {
if (empty($key) || empty($value)) {
continue;
}
$builder->add('action_' . $i++, ButtonType::class, [
'label' => $key,
'attr' => [
'data-href' => $value,
'class' => 'multi_update_table_action' . (stripos($key, 'delete') !== false ? ' btn-danger' : ''),
],
]);
}
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['repository']);
$resolver->setAllowedTypes('repository', EntityRepository::class);

View File

@@ -9,34 +9,33 @@
namespace App\Form\MultiUpdate;
use Doctrine\Common\Collections\Collection;
class MultiUpdateTableDTO
{
/**
* @var object[]
* @var iterable<object>|Collection<object>
*/
private $entities = [];
private iterable|Collection $entities = [];
/**
* @var string[]
*/
private $actions = ['' => ''];
/**
* @var string
*/
private $action = null;
private array $actions = ['' => ''];
private ?string $action = null;
/**
* @return object[]
*/
public function getEntities(): iterable
public function getEntities(): iterable|Collection
{
return $this->entities;
}
/**
* @param object[] $entities
* @param iterable<object>|Collection<object> $entities
* @return MultiUpdateTableDTO
*/
public function setEntities(iterable $entities): MultiUpdateTableDTO
public function setEntities(iterable|Collection $entities): MultiUpdateTableDTO
{
$this->entities = $entities;
@@ -65,7 +64,7 @@ class MultiUpdateTableDTO
public function addDelete(string $url): MultiUpdateTableDTO
{
$this->actions['action.delete'] = $url;
$this->actions['delete'] = $url;
return $this;
}

View File

@@ -30,27 +30,13 @@ use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimesheetMultiUpdate extends AbstractType
final class TimesheetMultiUpdate extends AbstractType
{
/**
* @var TimesheetRepository
*/
private $timesheet;
/**
* @var CustomerRepository
*/
private $customers;
public function __construct(TimesheetRepository $timesheet, CustomerRepository $customer)
public function __construct(private TimesheetRepository $timesheet, private CustomerRepository $customers)
{
$this->timesheet = $timesheet;
$this->customers = $customer;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$activity = null;
$project = null;
@@ -65,7 +51,7 @@ class TimesheetMultiUpdate extends AbstractType
$activity = $entry->getActivity();
$project = $entry->getProject();
$customer = null === $project ? null : $project->getCustomer();
$customer = $project?->getCustomer();
if (null === $project && null !== $activity) {
$project = $activity->getProject();
@@ -80,7 +66,7 @@ class TimesheetMultiUpdate extends AbstractType
->add('customer', CustomerType::class, [
'query_builder_for_user' => true,
'customers' => $customer,
'data' => $customer ? $customer : '',
'data' => $customer,
'required' => false,
'placeholder' => '',
'mapped' => false,
@@ -156,8 +142,8 @@ class TimesheetMultiUpdate extends AbstractType
'required' => true,
'expanded' => true,
'choices' => [
'label.replaceTags' => true,
'label.appendTags' => false,
'replaceTags' => true,
'appendTags' => false,
]
]);
@@ -173,7 +159,7 @@ class TimesheetMultiUpdate extends AbstractType
if ($options['include_exported']) {
$builder->add('exported', ChoiceType::class, [
'label' => 'label.mark_as_exported',
'label' => 'mark_as_exported',
'required' => false,
'choices' => [
'entryState.exported' => true,
@@ -184,7 +170,7 @@ class TimesheetMultiUpdate extends AbstractType
if ($options['include_billable']) {
$builder->add('billable', ChoiceType::class, [
'label' => 'label.billable',
'label' => 'billable',
'choices' => [
'' => null,
'yes' => true,
@@ -196,7 +182,7 @@ class TimesheetMultiUpdate extends AbstractType
if ($options['include_rate']) {
$builder
->add('recalculateRates', YesNoType::class, [
'label' => 'label.recalculate_rates',
'label' => 'recalculate_rates',
])
->add('fixedRate', FixedRateType::class, [
'currency' => $currency,
@@ -221,7 +207,7 @@ class TimesheetMultiUpdate extends AbstractType
'choices' => $choices,
'multiple' => true,
'expanded' => true,
'label' => 'label.batch_meta_fields',
'label' => 'batch_meta_fields',
'help' => 'help.batch_meta_fields',
]);
}
@@ -232,7 +218,7 @@ class TimesheetMultiUpdate extends AbstractType
$builder->get('entities')->addModelTransformer(
new CallbackTransformer(
function ($timesheets) {
function ($timesheets): string {
$ids = [];
/** @var \App\Entity\Timesheet $timesheet */
foreach ($timesheets as $timesheet) {
@@ -252,10 +238,7 @@ class TimesheetMultiUpdate extends AbstractType
);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => TimesheetMultiUpdateDTO::class,

View File

@@ -17,67 +17,38 @@ use App\Entity\Project;
use App\Entity\Tag;
use App\Entity\TimesheetMeta;
use App\Entity\User;
use App\Validator\Constraints as Constraints;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* @App\Validator\Constraints\TimesheetMultiUpdate
* @internal
*/
class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithMetaFields
#[Constraints\TimesheetMultiUpdate]
final class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithMetaFields
{
/**
* @var Tag[]|ArrayCollection|iterable
* @var iterable<Tag>
*/
private $tags = [];
private iterable $tags = [];
private bool $replaceTags = false;
private bool $recalculateRates = false;
private ?Customer $customer = null;
private ?Project $project = null;
private ?Activity $activity = null;
private ?User $user = null;
private ?bool $exported = null;
private ?bool $billable = null;
private ?float $fixedRate = null;
private ?float $hourlyRate = null;
/**
* @var bool
* @var Collection<TimesheetMeta>
*/
private $replaceTags = false;
private Collection $meta;
/**
* @var bool
* @var array<string>
*/
private $recalculateRates = false;
/**
* @var Customer|null
*/
private $customer;
/**
* @var Project|null
*/
private $project;
/**
* @var Activity|null
*/
private $activity;
/**
* @var User|null
*/
private $user;
/**
* @var bool|null
*/
private $exported = null;
/**
* @var bool|null
*/
private $billable = null;
/**
* @var float|null
*/
private $fixedRate = null;
/**
* @var float|null
*/
private $hourlyRate = null;
/**
* @var TimesheetMeta[]|Collection
*/
private $meta;
/**
* @var string[]
*/
private $updateMeta = [];
private array $updateMeta = [];
public function __construct()
{
@@ -115,7 +86,7 @@ class TimesheetMultiUpdateDTO extends MultiUpdateTableDTO implements EntityWithM
}
/**
* @return Tag[]|ArrayCollection|iterable
* @return iterable<Tag>
*/
public function getTags(): iterable
{

View File

@@ -16,12 +16,9 @@ use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PasswordResetForm extends AbstractType
final class PasswordResetForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
@@ -30,16 +27,13 @@ class PasswordResetForm extends AbstractType
'autocomplete' => 'new-password',
],
],
'first_options' => ['label' => 'label.password'],
'second_options' => ['label' => 'label.password_repeat'],
'first_options' => ['label' => 'password'],
'second_options' => ['label' => 'password_repeat'],
'invalid_message' => 'The entered passwords don\'t match.',
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
@@ -47,10 +41,7 @@ class PasswordResetForm extends AbstractType
]);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
public function getBlockPrefix(): string
{
return 'password_resetting';
}

View File

@@ -15,12 +15,9 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectCommentForm extends AbstractType
final class ProjectCommentForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('message', TextareaType::class, [
@@ -29,10 +26,7 @@ class ProjectCommentForm extends AbstractType
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ProjectComment::class,

View File

@@ -12,7 +12,9 @@ namespace App\Form;
use App\Entity\Customer;
use App\Entity\Project;
use App\Form\Type\CustomerType;
use App\Form\Type\DateTimePickerType;
use App\Form\Type\DatePickerType;
use App\Form\Type\InvoiceLabelType;
use App\Form\Type\TeamType;
use App\Form\Type\YesNoType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@@ -24,19 +26,16 @@ class ProjectEditForm extends AbstractType
{
use EntityFormTrait;
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$customer = null;
$id = null;
$isNew = false;
$options['currency'] = null;
if (isset($options['data'])) {
/** @var Project $entry */
$entry = $options['data'];
$id = $entry->getId();
$isNew = $entry->getId() === null;
if (null !== $entry->getCustomer()) {
$customer = $entry->getCustomer();
@@ -53,62 +52,63 @@ class ProjectEditForm extends AbstractType
$dateTimeOptions['format'] = $options['date_format'];
}
$timeIncrement = 1;
if ($options['time_increment'] >= 1 && $options['time_increment'] <= 60) {
$timeIncrement = $options['time_increment'];
}
$builder
->add('name', TextType::class, [
'label' => 'label.name',
'label' => 'name',
'attr' => [
'autofocus' => 'autofocus'
],
])
->add('comment', TextareaType::class, [
'label' => 'label.description',
'required' => false,
])
->add('invoiceText', TextareaType::class, [
'label' => 'label.invoiceText',
'label' => 'description',
'required' => false,
])
->add('invoiceText', InvoiceLabelType::class)
->add('orderNumber', TextType::class, [
'label' => 'label.orderNumber',
'label' => 'orderNumber',
'required' => false,
])
->add('orderDate', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.orderDate',
->add('orderDate', DatePickerType::class, array_merge($dateTimeOptions, [
'label' => 'orderDate',
'required' => false,
'time_increment' => $timeIncrement,
'force_time' => 'start',
]))
->add('start', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.project_start',
->add('start', DatePickerType::class, array_merge($dateTimeOptions, [
'label' => 'project_start',
'required' => false,
'time_increment' => $timeIncrement,
'force_time' => 'start',
]))
->add('end', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.project_end',
->add('end', DatePickerType::class, array_merge($dateTimeOptions, [
'label' => 'project_end',
'required' => false,
'time_increment' => $timeIncrement,
'force_time' => 'end',
]))
->add('customer', CustomerType::class, [
'placeholder' => (null === $id && null === $customer) ? '' : false,
'placeholder' => ($isNew && null === $customer) ? '' : false,
'customers' => $customer,
'query_builder_for_user' => true,
])
->add('globalActivities', YesNoType::class, [
'label' => 'label.globalActivities',
'label' => 'globalActivities',
'help' => 'help.globalActivities'
])
;
if ($isNew) {
$builder
->add('teams', TeamType::class, [
'required' => false,
'multiple' => true,
'expanded' => false,
'by_reference' => false,
'help' => 'help.teams',
]);
}
$this->addCommonFields($builder, $options);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Project::class,
@@ -120,7 +120,6 @@ class ProjectEditForm extends AbstractType
'include_budget' => false,
'include_time' => false,
'timezone' => date_default_timezone_get(),
'time_increment' => 1,
'attr' => [
'data-form-event' => 'kimai.projectUpdate'
],

View File

@@ -15,10 +15,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectRateForm extends AbstractRateForm
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$currency = null;
@@ -34,10 +31,7 @@ class ProjectRateForm extends AbstractRateForm
$this->addFields($builder, $currency);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ProjectRate::class,

View File

@@ -15,12 +15,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ProjectTeamPermissionForm extends AbstractType
final class ProjectTeamPermissionForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('teams', TeamType::class, [
@@ -31,10 +28,7 @@ class ProjectTeamPermissionForm extends AbstractType
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Project::class,

View File

@@ -23,19 +23,13 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\Valid;
class QuickEntryForm extends AbstractType
final class QuickEntryForm extends AbstractType
{
private $configuration;
public function __construct(SystemConfiguration $configuration)
public function __construct(private SystemConfiguration $configuration)
{
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new CallbackTransformer(
function ($value) {
@@ -92,10 +86,7 @@ class QuickEntryForm extends AbstractType
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$start = new \DateTime();
$start->setTime(0, 0, 0);

View File

@@ -10,35 +10,29 @@
namespace App\Form;
use App\Entity\Role;
use FOS\RestBundle\Validator\Constraints\Regex;
use App\Validator\Constraints\RoleName;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* The form used to edit roles.
*/
class RoleType extends AbstractType
final class RoleType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'label.name',
'help' => 'Allowed character: A-Z and _',
'constraints' => [
new Regex(['pattern' => '/^[a-zA-Z_]{5,}$/'])
],
'attr' => [
'maxlength' => 50
]
])
;
$builder->add('name', TextType::class, [
'label' => 'name',
'help' => 'Allowed character: A-Z and _',
'constraints' => [
new NotBlank(),
new RoleName(),
],
'attr' => [
'maxlength' => 50
]
]);
// help the user to figure out the allowed name
$builder->get('name')->addViewTransformer(
@@ -47,7 +41,6 @@ class RoleType extends AbstractType
if (\is_string($roleName)) {
$roleName = str_replace(' ', '_', $roleName);
$roleName = str_replace('-', '_', $roleName);
$roleName = strtoupper($roleName);
}
return $roleName;
@@ -59,10 +52,7 @@ class RoleType extends AbstractType
);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Role::class,

View File

@@ -17,16 +17,13 @@ use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SelfRegistrationForm extends AbstractType
final class SelfRegistrationForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email', EmailType::class, ['label' => 'label.email'])
->add('username', null, ['label' => 'Username', 'translation_domain' => 'AdminLTEBundle'])
->add('email', EmailType::class, ['label' => 'email'])
->add('username', null, ['label' => 'Username', 'translation_domain' => 'TablerBundle'])
->add('plainPassword', RepeatedType::class, [
'type' => PasswordType::class,
'options' => [
@@ -34,17 +31,14 @@ class SelfRegistrationForm extends AbstractType
'autocomplete' => 'new-password',
],
],
'first_options' => ['label' => 'label.password'],
'second_options' => ['label' => 'label.password_repeat'],
'first_options' => ['label' => 'password'],
'second_options' => ['label' => 'password_repeat'],
'invalid_message' => 'The entered passwords don\'t match.',
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
@@ -52,10 +46,7 @@ class SelfRegistrationForm extends AbstractType
]);
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
public function getBlockPrefix(): string
{
return 'user_registration';
}

View File

@@ -19,12 +19,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used to edit one section within the system configuration.
*/
class SystemConfigurationForm extends AbstractType
final class SystemConfigurationForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('configuration', CollectionType::class, [
'entry_type' => SystemConfigurationType::class,
@@ -35,10 +32,7 @@ class SystemConfigurationForm extends AbstractType
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => SystemConfiguration::class,

View File

@@ -19,14 +19,11 @@ class TagEditForm extends AbstractType
{
use ColorTrait;
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, [
'label' => 'label.name',
'label' => 'name',
'attr' => [
'autofocus' => 'autofocus'
],
@@ -39,10 +36,7 @@ class TagEditForm extends AbstractType
$this->addColor($builder);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Tag::class,

View File

@@ -15,12 +15,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TeamCustomerForm extends AbstractType
final class TeamCustomerForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('customers', CustomerType::class, [
@@ -33,10 +30,7 @@ class TeamCustomerForm extends AbstractType
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Team::class,

View File

@@ -22,10 +22,7 @@ class TeamEditForm extends AbstractType
{
use ColorTrait;
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$users = [];
@@ -37,7 +34,7 @@ class TeamEditForm extends AbstractType
$builder
->add('name', TextType::class, [
'label' => 'label.name',
'label' => 'name',
'attr' => [
'autofocus' => 'autofocus'
],
@@ -58,7 +55,8 @@ class TeamEditForm extends AbstractType
'allow_add' => true,
'by_reference' => false,
'allow_delete' => true,
'label' => 'label.user',
'label' => 'team.member',
'translation_domain' => 'teams',
'documentation' => [
'type' => 'array',
'items' => [
@@ -89,10 +87,7 @@ class TeamEditForm extends AbstractType
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Team::class,

View File

@@ -15,12 +15,9 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TeamProjectForm extends AbstractType
final class TeamProjectForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('projects', ProjectType::class, [
@@ -34,10 +31,7 @@ class TeamProjectForm extends AbstractType
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Team::class,

View File

@@ -13,7 +13,7 @@ use Symfony\Component\Form\FormBuilderInterface;
class TimesheetAdminEditForm extends TimesheetEditForm
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$options['allow_begin_datetime'] = true;
$options['allow_end_datetime'] = true;

View File

@@ -9,14 +9,16 @@
namespace App\Form;
use App\Configuration\SystemConfiguration;
use App\Entity\Timesheet;
use App\Form\Type\DateTimePickerType;
use App\Form\Type\DatePickerType;
use App\Form\Type\DescriptionType;
use App\Form\Type\DurationType;
use App\Form\Type\FixedRateType;
use App\Form\Type\HourlyRateType;
use App\Form\Type\MetaFieldsCollectionType;
use App\Form\Type\TagsType;
use App\Form\Type\TimePickerType;
use App\Form\Type\TimesheetBillableType;
use App\Form\Type\UserType;
use App\Form\Type\YesNoType;
@@ -28,6 +30,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;
/**
* Defines the form used to manipulate Timesheet entries.
@@ -36,20 +39,11 @@ class TimesheetEditForm extends AbstractType
{
use FormTrait;
/**
* @var CustomerRepository
*/
private $customers;
public function __construct(CustomerRepository $customer)
public function __construct(private CustomerRepository $customers, private SystemConfiguration $systemConfiguration)
{
$this->customers = $customer;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$activity = null;
$project = null;
@@ -65,7 +59,7 @@ class TimesheetEditForm extends AbstractType
$activity = $entry->getActivity();
$project = $entry->getProject();
$customer = null === $project ? null : $project->getCustomer();
$customer = $project?->getCustomer();
if (null !== $entry->getId()) {
$isNew = false;
@@ -110,8 +104,12 @@ class TimesheetEditForm extends AbstractType
$this->addCustomer($builder, $customer);
}
$allowCreate = (bool) $this->systemConfiguration->find('activity.allow_inline_create');
$this->addProject($builder, $isNew, $project, $customer);
$this->addActivity($builder, $activity, $project);
$this->addActivity($builder, $activity, $project, [
'allow_create' => $allowCreate && $options['create_activity'],
]);
$descriptionOptions = ['required' => false];
if (!$isNew) {
@@ -144,34 +142,129 @@ class TimesheetEditForm extends AbstractType
return true;
}
protected function addBegin(FormBuilderInterface $builder, array $dateTimeOptions, array $options = [])
protected function addBegin(FormBuilderInterface $builder, array $dateTimeOptions, array $options = []): void
{
if ($options['begin_minutes'] >= 1 && $options['begin_minutes'] <= 60) {
$dateTimeOptions['time_increment'] = $options['begin_minutes'];
}
$builder->add('begin', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.begin',
$dateOptions = $dateTimeOptions;
$builder->add('begin_date', DatePickerType::class, array_merge($dateOptions, [
'label' => 'date',
'mapped' => false,
'constraints' => [
new NotBlank()
]
]));
$timeOptions = $dateTimeOptions;
$builder->add('begin_time', TimePickerType::class, array_merge($timeOptions, [
'label' => 'starttime',
'mapped' => false,
'constraints' => [
new NotBlank()
]
]));
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
/** @var Timesheet $timesheet */
$timesheet = $event->getData();
$begin = $timesheet->getBegin();
if (null !== $begin) {
$event->getForm()->get('begin_date')->setData($begin);
$event->getForm()->get('begin_time')->setData($begin);
}
}
);
// map single fields to original datetime object
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
/** @var Timesheet $data */
$data = $event->getData();
/** @var \DateTime|null $date */
$date = $event->getForm()->get('begin_date')->getData();
$time = $event->getForm()->get('begin_time')->getData();
if ($date === null || $time === null) {
return;
}
// mutable datetime are a problem for doctrine
$newDate = clone $date;
$newDate->setTime($time->format('H'), $time->format('i'));
if ($data->getBegin() === null || $data->getBegin()->getTimestamp() !== $newDate->getTimestamp()) {
$data->setBegin($newDate);
}
}
);
}
protected function addEnd(FormBuilderInterface $builder, array $dateTimeOptions, array $options = [])
protected function addEnd(FormBuilderInterface $builder, array $dateTimeOptions, array $options = []): void
{
if ($options['end_minutes'] >= 1 && $options['end_minutes'] <= 60) {
$dateTimeOptions['time_increment'] = (int) $options['end_minutes'];
}
$builder->add('end', DateTimePickerType::class, array_merge($dateTimeOptions, [
'label' => 'label.end',
$builder->add('end_time', TimePickerType::class, array_merge($dateTimeOptions, [
'required' => false,
'label' => 'endtime',
'mapped' => false
]));
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
/** @var Timesheet|null $data */
$data = $event->getData();
if (null !== $data->getEnd()) {
$event->getForm()->get('end_time')->setData($data->getEnd());
}
}
);
// make sure that date & time fields are mapped back to begin & end fields
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) {
/** @var Timesheet $timesheet */
$timesheet = $event->getData();
$oldEnd = $timesheet->getEnd();
$end = $event->getForm()->get('end_time')->getData();
if ($end === null || $end === false) {
$timesheet->setEnd(null);
return;
}
// mutable datetime are a problem for doctrine
$end = clone $end;
// end is assumed to be the same day then start, if not we raise the day by one
//$time = $event->getForm()->get('begin_time')->getData();
$time = $timesheet->getBegin();
if ($time === null) {
throw new \Exception('Cannot work with timesheets without start time');
}
$newEnd = clone $time;
$newEnd->setTime($end->format('H'), $end->format('i'));
if ($newEnd < $time) {
$newEnd->modify('+ 1 day');
}
if ($oldEnd === null || $oldEnd->getTimestamp() !== $newEnd->getTimestamp()) {
$timesheet->setEnd($newEnd);
}
}
);
}
protected function addDuration(FormBuilderInterface $builder, array $options, bool $forceApply = false, bool $autofocus = false)
protected function addDuration(FormBuilderInterface $builder, array $options, bool $forceApply = false, bool $autofocus = false): void
{
$durationOptions = [
'required' => false,
'docu_chapter' => 'duration-format.html',
//'toggle' => true,
'attr' => [
'placeholder' => '0:00',
],
@@ -200,9 +293,9 @@ class TimesheetEditForm extends AbstractType
$builder->addEventListener(
FormEvents::POST_SET_DATA,
function (FormEvent $event) {
/** @var Timesheet|null $data */
$data = $event->getData();
if (null === $data || null === $data->getEnd()) {
/** @var Timesheet|null $timesheet */
$timesheet = $event->getData();
if (null === $timesheet || null === $timesheet->getEnd()) {
$event->getForm()->get('duration')->setData(null);
}
}
@@ -212,21 +305,31 @@ class TimesheetEditForm extends AbstractType
$builder->addEventListener(
FormEvents::SUBMIT,
function (FormEvent $event) use ($forceApply) {
/** @var Timesheet $data */
$data = $event->getData();
$duration = $data->getDuration();
/** @var Timesheet $timesheet */
$timesheet = $event->getData();
$newDuration = $event->getForm()->get('duration')->getData();
if ($newDuration !== null && $newDuration > 0 && $newDuration !== $timesheet->getDuration()) {
// TODO allow to use a duration that differs from end-start by adding a system configuration check here
if ($timesheet->getEnd() === null) {
$timesheet->setDuration($newDuration);
}
}
$duration = $timesheet->getDuration() ?? 0;
// only apply the duration, if the end is not yet set
// without that check, the end would be overwritten and the real end time would be lost
if (($forceApply && null !== $duration) || (null !== $duration && null === $data->getEnd())) {
$end = clone $data->getBegin();
if (($forceApply && $duration > 0) || ($duration > 0 && null === $timesheet->getEnd())) {
$end = clone $timesheet->getBegin();
$end->modify('+ ' . $duration . 'seconds');
$data->setEnd($end);
$timesheet->setEnd($end);
}
}
);
}
protected function addRates(FormBuilderInterface $builder, $currency, array $options)
protected function addRates(FormBuilderInterface $builder, $currency, array $options): void
{
if (!$options['include_rate']) {
return;
@@ -241,7 +344,7 @@ class TimesheetEditForm extends AbstractType
]);
}
protected function addUser(FormBuilderInterface $builder, array $options)
protected function addUser(FormBuilderInterface $builder, array $options): void
{
if (!$options['include_user']) {
return;
@@ -250,18 +353,18 @@ class TimesheetEditForm extends AbstractType
$builder->add('user', UserType::class);
}
protected function addExported(FormBuilderInterface $builder, array $options)
protected function addExported(FormBuilderInterface $builder, array $options): void
{
if (!$options['include_exported']) {
return;
}
$builder->add('exported', YesNoType::class, [
'label' => 'label.exported'
'label' => 'exported'
]);
}
protected function addBillable(FormBuilderInterface $builder, array $options)
protected function addBillable(FormBuilderInterface $builder, array $options): void
{
if ($options['include_billable']) {
$builder->add('billableMode', TimesheetBillableType::class, []);
@@ -281,18 +384,21 @@ class TimesheetEditForm extends AbstractType
},
function (Timesheet $record) {
$billable = new BillableCalculator();
$billable->calculate($record);
$billable->calculate($record, []);
return $record;
}
));
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$maxMinutes = $this->systemConfiguration->getTimesheetLongRunningDuration();
$maxHours = 8;
if ($maxMinutes > 0) {
$maxHours = (int) ($maxMinutes / 60);
}
$resolver->setDefaults([
'data_class' => Timesheet::class,
'csrf_protection' => true,
@@ -302,6 +408,7 @@ class TimesheetEditForm extends AbstractType
'include_exported' => false,
'include_billable' => true,
'include_rate' => true,
'create_activity' => false,
'docu_chapter' => 'timesheet.html',
'method' => 'POST',
'date_format' => null,
@@ -311,9 +418,7 @@ class TimesheetEditForm extends AbstractType
'allow_end_datetime' => true,
'allow_duration' => false,
'duration_minutes' => null,
'duration_hours' => 10,
'begin_minutes' => 1,
'end_minutes' => 1,
'duration_hours' => $maxHours,
'attr' => [
'data-form-event' => 'kimai.timesheetUpdate',
'data-msg-success' => 'action.update.success',

View File

@@ -12,11 +12,10 @@ namespace App\Form;
use App\Form\Type\TeamType;
use App\Form\Type\UserType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TimesheetMultiUserEditForm extends TimesheetAdminEditForm
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$options['allow_begin_datetime'] = true;
$options['allow_end_datetime'] = true;
@@ -25,6 +24,7 @@ class TimesheetMultiUserEditForm extends TimesheetAdminEditForm
parent::buildForm($builder, $options);
$builder->add('users', UserType::class, [
'label' => 'users',
'multiple' => true,
'required' => false,
]);
@@ -34,9 +34,4 @@ class TimesheetMultiUserEditForm extends TimesheetAdminEditForm
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
}
}

View File

@@ -19,11 +19,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Values that are allowed to be pre-set via URL.
*/
class TimesheetPreCreateForm extends AbstractType
final class TimesheetPreCreateForm extends AbstractType
{
use FormTrait;
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addProject($builder, true, null, null, ['required' => false]);
$this->addActivity($builder, null, null, ['required' => false]);
@@ -34,7 +34,7 @@ class TimesheetPreCreateForm extends AbstractType
}
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'csrf_protection' => false,

View File

@@ -9,454 +9,25 @@
namespace App\Form\Toolbar;
use App\Entity\Activity;
use App\Form\Type\ActivityType;
use App\Form\Type\CustomerType;
use App\Form\Type\DateRangeType;
use App\Form\Type\PageSizeType;
use App\Form\Type\ProjectType;
use App\Form\Type\SearchTermType;
use App\Form\Type\TagsType;
use App\Form\Type\TeamType;
use App\Form\Type\UserRoleType;
use App\Form\Type\UserType;
use App\Form\Type\VisibilityType;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\BaseQuery;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectFormTypeQuery;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Validator\Constraints\Choice;
/**
* Defines the base form used for all toolbars.
*
* Extend this class and stack the elements defined here, they are coupled to each other and with the toolbar.js.
* Extend this class and stack the elements defined here, they are coupled to each other and connected with javascript.
*
* @deprecated since 2.0, will be removed with 2.1 - use ToolbarFormTrait instead
*/
abstract class AbstractToolbarForm extends AbstractType
{
/**
* Dirty hack to enable easy handling of GET form in controller and javascript.
* Cleans up the name of all form elements (and unfortunately of the form itself).
*
* @return null|string
*/
public function getBlockPrefix()
use ToolbarFormTrait;
public function getBlockPrefix(): string
{
@trigger_error('The "AbstractToolbarForm" is deprecated and will be removed with 2.1, use the "ToolbarFormTrait" instead', E_USER_DEPRECATED);
// Dirty hack to enable easy handling of GET form in controller and javascript.
// Cleans up the name of all form elements (and unfortunately of the form itself).
return '';
}
protected function addUserChoice(FormBuilderInterface $builder)
{
$builder->add('user', UserType::class, [
'label' => 'label.user',
'required' => false,
]);
}
protected function addUsersChoice(FormBuilderInterface $builder, string $field = 'users', array $options = [])
{
$builder->add($field, UserType::class, array_merge([
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'User ID'],
'description' => 'Array of user IDs',
],
'label' => 'label.user',
'multiple' => true,
'required' => false,
], $options));
}
protected function addTeamChoice(FormBuilderInterface $builder)
{
$builder->add('team', TeamType::class, [
'label' => 'label.team',
'required' => false,
]);
}
protected function addTeamsChoice(FormBuilderInterface $builder, string $field = 'teams', array $options = [])
{
$builder->add($field, TeamType::class, array_merge([
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Team ID'],
'description' => 'Array of team IDs',
],
'label' => 'label.team',
'multiple' => true,
'required' => false,
], $options));
}
protected function addCustomerChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false)
{
$this->addCustomerSelect($builder, $options, false, $multiProject);
}
protected function addCustomerMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false)
{
$this->addCustomerSelect($builder, $options, true, $multiProject);
}
private function addCustomerSelect(FormBuilderInterface $builder, array $options, bool $multiCustomer, bool $multiProject)
{
$name = 'customer';
if ($multiCustomer) {
$name = 'customers';
}
// just a fake field for having this field at the right position in the frontend
$builder->add($name, CustomerType::class, [
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Customer ID'],
'description' => 'Array of customer IDs',
],
'choices' => [],
'multiple' => $multiCustomer,
]);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $options, $name, $multiCustomer, $multiProject) {
$data = $event->getData();
$event->getForm()->add($name, CustomerType::class, array_merge([
'multiple' => $multiCustomer,
'required' => false,
'project_enabled' => $multiCustomer ? 'customers' : 'customer',
'project_select' => $multiProject ? 'projects' : 'project',
'end_date_param' => '%daterange%',
'start_date_param' => '%daterange%',
'query_builder' => function (CustomerRepository $repo) use ($builder, $data, $name, $multiCustomer) {
$query = new CustomerFormTypeQuery();
$query->setUser($builder->getOption('user'));
if (isset($data[$name]) && !empty($data[$name])) {
if ($multiCustomer) {
$query->setCustomers($data[$name]);
} else {
$query->addCustomer($data[$name]);
}
}
return $repo->getQueryBuilderForFormType($query);
},
], $options));
}
);
}
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'label.visible')
{
$builder->add('visibility', VisibilityType::class, [
'required' => false,
'placeholder' => null,
'label' => $label,
'search' => false
]);
}
protected function addPageSizeChoice(FormBuilderInterface $builder)
{
$builder->add('pageSize', PageSizeType::class, [
'required' => false,
'search' => false
]);
}
protected function addUserRoleChoice(FormBuilderInterface $builder)
{
$builder->add('role', UserRoleType::class, [
'required' => false,
]);
}
protected function addDateRange(FormBuilderInterface $builder, array $options, $allowEmpty = true, $required = false)
{
$params = [
'required' => $required,
'allow_empty' => $allowEmpty,
];
if (\array_key_exists('timezone', $options)) {
$params['timezone'] = $options['timezone'];
}
$builder->add('daterange', DateRangeType::class, $params);
}
protected function addDateRangeChoice(FormBuilderInterface $builder, $allowEmpty = true, $required = false)
{
$this->addDateRange($builder, [], $allowEmpty, $required);
}
protected function addProjectChoice(FormBuilderInterface $builder, array $options = [], bool $multiCustomer = false, bool $multiActivity = false)
{
$this->addProjectSelect($builder, $options, false, $multiCustomer, $multiActivity);
}
protected function addProjectMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiCustomer = false, bool $multiActivity = false)
{
$this->addProjectSelect($builder, $options, true, $multiCustomer, $multiActivity);
}
private function addProjectSelect(FormBuilderInterface $builder, array $options, bool $multiProject, bool $multiCustomer, bool $multiActivity)
{
$name = 'project';
if ($multiProject) {
$name = 'projects';
}
// just a fake field for having this field at the right position in the frontend
$builder->add($name, ProjectType::class, [
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Project ID'],
'description' => 'Array of project IDs',
],
'choices' => [],
'multiple' => $multiProject,
]);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $options, $name, $multiCustomer, $multiProject, $multiActivity) {
$data = $event->getData();
$event->getForm()->add($name, ProjectType::class, array_merge([
'multiple' => $multiProject,
'required' => false,
'activity_enabled' => $multiProject ? 'projects' : 'project',
'activity_select' => $multiActivity ? 'activities' : 'activity',
'query_builder' => function (ProjectRepository $repo) use ($builder, $data, $options, $multiCustomer, $multiProject) {
$query = new ProjectFormTypeQuery();
$query->setUser($builder->getOption('user'));
$name = $multiCustomer ? 'customers' : 'customer';
if (isset($data[$name]) && !empty($data[$name])) {
if (\is_array($data[$name])) {
$query->setCustomers($data[$name]);
} else {
$query->addCustomer($data[$name]);
}
}
$name = $multiProject ? 'projects' : 'project';
if (isset($data[$name]) && !empty($data[$name])) {
if (\is_array($data[$name])) {
$query->setProjects($data[$name]);
} else {
$query->addProject($data[$name]);
}
}
if (isset($options['ignore_date']) && true === $options['ignore_date']) {
$query->setIgnoreDate(true);
}
return $repo->getQueryBuilderForFormType($query);
},
], $options));
}
);
}
protected function addActivityChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false)
{
$this->addActivitySelect($builder, $options, false, $multiProject);
}
protected function addActivityMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false)
{
$this->addActivitySelect($builder, $options, true, $multiProject);
}
private function addActivitySelect(FormBuilderInterface $builder, array $options = [], bool $multiActivity = false, bool $multiProject = false)
{
$name = 'activity';
if ($multiActivity) {
$name = 'activities';
}
// just a fake field for having this field at the right position in the frontend
$builder->add($name, ActivityType::class, [
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Activity ID'],
'description' => 'Array of activity IDs',
],
'choices' => [],
'multiple' => $multiActivity,
]);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($name, $multiActivity, $multiProject) {
$data = $event->getData();
$event->getForm()->add($name, ActivityType::class, [
'multiple' => $multiActivity,
'required' => false,
'query_builder' => function (ActivityRepository $repo) use ($data, $multiActivity, $multiProject) {
$query = new ActivityFormTypeQuery();
$name = $multiActivity ? 'activities' : 'activity';
if (isset($data[$name]) && !empty($data[$name])) {
// we need to pre-fetch the activities to see if they are global, see ActivityFormTypeQuery::isGlobalsOnly()
$activities = $data[$name];
if (!\is_array($activities)) {
$activities = [$activities];
}
foreach ($activities as $activity) {
if ($activity instanceof Activity) {
$query->addActivity($activity);
} elseif ($activity !== null) {
$tmp = $repo->find($activity);
if (null !== $tmp) {
$query->addActivity($tmp);
}
}
}
}
$name = $multiProject ? 'projects' : 'project';
if (isset($data[$name]) && !empty($data[$name])) {
if ($multiProject) {
$query->setProjects($data[$name]);
} else {
$query->addProject($data[$name]);
}
}
return $repo->getQueryBuilderForFormType($query);
},
]);
}
);
}
protected function addHiddenPagination(FormBuilderInterface $builder)
{
$builder->add('page', HiddenType::class, [
'documentation' => [
'type' => 'integer',
'description' => 'Page number. Default: 1',
],
'empty_data' => 1
]);
}
protected function addHiddenOrder(FormBuilderInterface $builder)
{
@trigger_error('addHiddenOrder() is deprecated and will be removed with 2.0, use the new search modal instead', E_USER_DEPRECATED);
$builder->add('order', HiddenType::class, [
'documentation' => [
'type' => 'string',
'description' => 'The order for returned items',
],
'constraints' => [
new Choice(['choices' => [BaseQuery::ORDER_ASC, BaseQuery::ORDER_DESC]])
]
]);
}
protected function addOrder(FormBuilderInterface $builder)
{
$builder->add('order', ChoiceType::class, [
'documentation' => [
'description' => 'The order for returned items',
],
'label' => 'label.order',
'choices' => [
'label.asc' => BaseQuery::ORDER_ASC,
'label.desc' => BaseQuery::ORDER_DESC
],
'search' => false,
]);
}
protected function addHiddenOrderBy(FormBuilderInterface $builder, array $allowedColumns)
{
@trigger_error('addHiddenOrderBy() is deprecated and will be removed with 2.0, use the new search modal instead', E_USER_DEPRECATED);
$builder->add('orderBy', HiddenType::class, [
'constraints' => [
new Choice(['choices' => $allowedColumns])
]
]);
}
protected function addOrderBy(FormBuilderInterface $builder, array $allowedColumns)
{
$all = [];
foreach ($allowedColumns as $id => $name) {
$label = \is_int($id) ? 'label.' . $name : $id;
$all[$label] = $name;
}
$builder->add('orderBy', ChoiceType::class, [
'label' => 'label.orderBy',
'choices' => $all,
'search' => false,
]);
}
protected function addTagInputField(FormBuilderInterface $builder)
{
$builder->add('tags', TagsType::class, [
'required' => false
]);
}
protected function addSearchTermInputField(FormBuilderInterface $builder)
{
$builder->add('searchTerm', SearchTermType::class);
}
protected function addTimesheetStateChoice(FormBuilderInterface $builder)
{
$builder->add('state', ChoiceType::class, [
'label' => 'label.entryState',
'required' => false,
'placeholder' => null,
'search' => false,
'choices' => [
'entryState.all' => TimesheetQuery::STATE_ALL,
'entryState.running' => TimesheetQuery::STATE_RUNNING,
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
],
]);
}
protected function addExportStateChoice(FormBuilderInterface $builder)
{
$builder->add('exported', ChoiceType::class, [
'label' => 'label.exported',
'required' => false,
'placeholder' => null,
'search' => false,
'choices' => [
'entryState.all' => TimesheetQuery::STATE_ALL,
'entryState.exported' => TimesheetQuery::STATE_EXPORTED,
'entryState.not_exported' => TimesheetQuery::STATE_NOT_EXPORTED
],
]);
}
protected function addBillableChoice(FormBuilderInterface $builder)
{
$builder->add('billable', BillableType::class, [
'required' => false,
'placeholder' => null,
'search' => false,
]);
}
}

View File

@@ -10,6 +10,7 @@
namespace App\Form\Toolbar;
use App\Repository\Query\ActivityQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -17,12 +18,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the activities.
*/
class ActivityToolbarForm extends AbstractToolbarForm
final class ActivityToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$newOptions = [];
if ($options['ignore_date'] === true) {
@@ -40,7 +40,7 @@ class ActivityToolbarForm extends AbstractToolbarForm
'search' => false,
'placeholder' => null,
'required' => false,
'label' => 'label.globalsOnly',
'label' => 'globalsOnly',
]);
$this->addVisibilityChoice($builder);
$this->addPageSizeChoice($builder);
@@ -49,10 +49,7 @@ class ActivityToolbarForm extends AbstractToolbarForm
$this->addOrderBy($builder, ActivityQuery::ACTIVITY_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ActivityQuery::class,

View File

@@ -10,18 +10,18 @@
namespace App\Form\Toolbar;
use App\Repository\Query\CustomerQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the customer.
*/
class CustomerToolbarForm extends AbstractToolbarForm
final class CustomerToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addVisibilityChoice($builder);
@@ -31,10 +31,7 @@ class CustomerToolbarForm extends AbstractToolbarForm
$this->addOrderBy($builder, CustomerQuery::CUSTOMER_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => CustomerQuery::class,

View File

@@ -9,8 +9,8 @@
namespace App\Form\Toolbar;
use App\Form\Type\MarkAsExportedType;
use App\Repository\Query\ExportQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -18,44 +18,35 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering timesheet entries for exports.
*/
class ExportToolbarForm extends AbstractToolbarForm
final class ExportToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addBillableChoice($builder);
$this->addExportStateChoice($builder);
$this->addTimesheetStateChoice($builder);
if ($options['include_user']) {
$this->addUsersChoice($builder);
$this->addTeamsChoice($builder);
}
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
$this->addActivityMultiChoice($builder, [], true);
$this->addExportRenderer($builder);
$this->addTagInputField($builder);
if ($options['include_user']) {
$this->addUsersChoice($builder);
$this->addTeamsChoice($builder);
}
$this->addTimesheetStateChoice($builder);
$this->addBillableChoice($builder);
$this->addExportStateChoice($builder);
$builder->add('renderer', HiddenType::class, []);
if ($options['include_export']) {
$builder->add('markAsExported', MarkAsExportedType::class);
$builder->add('markAsExported', HiddenType::class, [
'label' => 'mark_as_exported',
'required' => false,
]);
}
}
/**
* @param FormBuilderInterface $builder
*/
protected function addExportRenderer(FormBuilderInterface $builder)
{
$builder->add('renderer', HiddenType::class, []);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ExportQuery::class,

View File

@@ -11,32 +11,30 @@ namespace App\Form\Toolbar;
use App\Form\Type\InvoiceStatusType;
use App\Repository\Query\InvoiceArchiveQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering timesheet entries for invoices.
*/
class InvoiceArchiveForm extends AbstractToolbarForm
final class InvoiceArchiveForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['required' => false, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
$builder->add('status', InvoiceStatusType::class, ['required' => false]);
$this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder);
$this->addOrder($builder);
$this->addOrderBy($builder, InvoiceArchiveQuery::INVOICE_ARCHIVE_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => InvoiceArchiveQuery::class,

View File

@@ -9,20 +9,26 @@
namespace App\Form\Toolbar;
use App\Form\Type\DatePickerType;
use App\Form\Type\InvoiceTemplateType;
use App\Repository\Query\InvoiceQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering timesheet entries for invoices.
*/
class InvoiceToolbarForm extends InvoiceToolbarSimpleForm
final class InvoiceToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
parent::buildForm($builder, $options);
$this->addTemplateChoice($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['required' => false, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], false, true);
$this->addSearchTermInputField($builder);
if ($options['include_user']) {
$this->addUsersChoice($builder);
@@ -31,14 +37,26 @@ class InvoiceToolbarForm extends InvoiceToolbarSimpleForm
$this->addActivityMultiChoice($builder, $options, true);
$this->addTagInputField($builder);
$this->addExportStateChoice($builder);
$builder->add('invoiceDate', DatePickerType::class, [
'required' => true,
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
protected function addTemplateChoice(FormBuilderInterface $builder): void
{
parent::configureOptions($resolver);
$resolver->setDefault('include_user', true);
$builder->add('template', InvoiceTemplateType::class, [
'required' => true,
'placeholder' => null,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => InvoiceQuery::class,
'csrf_protection' => false,
'include_user' => true,
'timezone' => date_default_timezone_get(),
]);
}
}

View File

@@ -1,58 +0,0 @@
<?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\Toolbar;
use App\Form\Type\InvoiceTemplateType;
use App\Form\Type\MarkAsExportedType;
use App\Repository\Query\InvoiceQuery;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering timesheet entries for invoices.
*/
class InvoiceToolbarSimpleForm extends AbstractToolbarForm
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->addTemplateChoice($builder);
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
$this->addCustomerMultiChoice($builder, ['required' => false, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
$this->addProjectMultiChoice($builder, ['ignore_date' => true], true, true);
if ($options['include_export']) {
$builder->add('markAsExported', MarkAsExportedType::class);
}
}
protected function addTemplateChoice(FormBuilderInterface $builder)
{
$builder->add('template', InvoiceTemplateType::class, [
'required' => true,
'placeholder' => null,
]);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => InvoiceQuery::class,
'csrf_protection' => false,
'include_user' => true,
'include_export' => true,
'timezone' => date_default_timezone_get(),
]);
}
}

View File

@@ -10,18 +10,18 @@
namespace App\Form\Toolbar;
use App\Repository\Query\ProjectQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the projects.
*/
class ProjectToolbarForm extends AbstractToolbarForm
final class ProjectToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addCustomerMultiChoice($builder, [], false);
@@ -32,10 +32,7 @@ class ProjectToolbarForm extends AbstractToolbarForm
$this->addOrderBy($builder, ProjectQuery::PROJECT_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => ProjectQuery::class,

View File

@@ -10,15 +10,15 @@
namespace App\Form\Toolbar;
use App\Repository\Query\TagQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagToolbarForm extends AbstractToolbarForm
final class TagToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addPageSizeChoice($builder);
@@ -27,10 +27,7 @@ class TagToolbarForm extends AbstractToolbarForm
$this->addOrderBy($builder, TagQuery::TAG_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => TagQuery::class,

View File

@@ -10,15 +10,15 @@
namespace App\Form\Toolbar;
use App\Repository\Query\TeamQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TeamToolbarForm extends AbstractToolbarForm
final class TeamToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addUsersChoice($builder);
$this->addSearchTermInputField($builder);
@@ -28,10 +28,7 @@ class TeamToolbarForm extends AbstractToolbarForm
$this->addOrderBy($builder, TeamQuery::TEAM_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => TeamQuery::class,

View File

@@ -10,23 +10,18 @@
namespace App\Form\Toolbar;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the timesheet.
*/
class TimesheetExportToolbarForm extends AbstractToolbarForm
final class TimesheetExportToolbarForm extends AbstractType
{
public function getBlockPrefix()
{
return 'export';
}
use ToolbarFormTrait;
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$newOptions = [];
if ($options['ignore_date'] === true) {
@@ -49,10 +44,7 @@ class TimesheetExportToolbarForm extends AbstractToolbarForm
$this->addOrderBy($builder, TimesheetQuery::TIMESHEET_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => TimesheetQuery::class,

View File

@@ -10,18 +10,18 @@
namespace App\Form\Toolbar;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the timesheet.
*/
class TimesheetToolbarForm extends AbstractToolbarForm
final class TimesheetToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$newOptions = [];
if ($options['ignore_date'] === true) {
@@ -47,10 +47,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
$this->addOrderBy($builder, TimesheetQuery::TIMESHEET_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => TimesheetQuery::class,

View File

@@ -0,0 +1,436 @@
<?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\Toolbar;
use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Form\Type\ActivityType;
use App\Form\Type\BillableSearchType;
use App\Form\Type\CustomerType;
use App\Form\Type\DateRangeType;
use App\Form\Type\PageSizeType;
use App\Form\Type\ProjectType;
use App\Form\Type\SearchTermType;
use App\Form\Type\TagsType;
use App\Form\Type\TeamType;
use App\Form\Type\UserRoleType;
use App\Form\Type\UserType;
use App\Form\Type\VisibilityType;
use App\Repository\ActivityRepository;
use App\Repository\CustomerRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\ActivityFormTypeQuery;
use App\Repository\Query\BaseQuery;
use App\Repository\Query\CustomerFormTypeQuery;
use App\Repository\Query\ProjectFormTypeQuery;
use App\Repository\Query\TimesheetQuery;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
/**
* Defines the base form used for all toolbars.
*
* Extend this class and stack the elements defined here, they are coupled to each other and with the toolbar.js.
*/
trait ToolbarFormTrait
{
protected function addUserChoice(FormBuilderInterface $builder): void
{
$builder->add('user', UserType::class, [
'label' => 'user',
'required' => false,
]);
}
protected function addUsersChoice(FormBuilderInterface $builder, string $field = 'users', array $options = []): void
{
$builder->add($field, UserType::class, array_merge([
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'User ID'],
'description' => 'Array of user IDs',
],
'label' => 'user',
'multiple' => true,
'required' => false,
], $options));
}
protected function addTeamChoice(FormBuilderInterface $builder): void
{
$builder->add('team', TeamType::class, [
'label' => 'team',
'required' => false,
]);
}
protected function addTeamsChoice(FormBuilderInterface $builder, string $field = 'teams', array $options = []): void
{
$builder->add($field, TeamType::class, array_merge([
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Team ID'],
'description' => 'Array of team IDs',
],
'label' => 'team',
'multiple' => true,
'required' => false,
], $options));
}
protected function addCustomerChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
{
$this->addCustomerSelect($builder, $options, false, $multiProject);
}
protected function addCustomerMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
{
$this->addCustomerSelect($builder, $options, true, $multiProject);
}
private function addCustomerSelect(FormBuilderInterface $builder, array $options, bool $multiCustomer, bool $multiProject): void
{
$name = 'customer';
if ($multiCustomer) {
$name = 'customers';
}
// just a fake field for having this field at the right position in the frontend
$builder->add($name, CustomerType::class, [
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Customer ID'],
'description' => 'Array of customer IDs',
],
'choices' => [],
'multiple' => $multiCustomer,
]);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $options, $name, $multiCustomer, $multiProject) {
/** @var array<string, mixed> $data */
$data = $event->getData();
$event->getForm()->add($name, CustomerType::class, array_merge([
'multiple' => $multiCustomer,
'required' => false,
'project_enabled' => $multiCustomer ? 'customers[]' : 'customer',
'project_select' => $multiProject ? 'projects' : 'project',
'end_date_param' => '%daterange%',
'start_date_param' => '%daterange%',
'query_builder' => function (CustomerRepository $repo) use ($builder, $data, $name) {
$query = new CustomerFormTypeQuery();
$query->setUser($builder->getOption('user'));
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
$customers = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
foreach ($customers as $customer) {
$customer = \is_string($customer) ? (int) $customer : $customer;
if (!\is_int($customer) && !($customer instanceof Customer)) {
throw new \Exception('Need a customer object or an ID for customer select');
}
$query->addCustomer($customer);
}
}
return $repo->getQueryBuilderForFormType($query);
},
], $options));
}
);
}
protected function addVisibilityChoice(FormBuilderInterface $builder, string $label = 'visible'): void
{
$builder->add('visibility', VisibilityType::class, [
'required' => false,
'placeholder' => null,
'label' => $label,
'search' => false
]);
}
protected function addPageSizeChoice(FormBuilderInterface $builder): void
{
$builder->add('pageSize', PageSizeType::class, [
'required' => false,
'search' => false
]);
}
protected function addUserRoleChoice(FormBuilderInterface $builder): void
{
$builder->add('role', UserRoleType::class, [
'required' => false,
]);
}
protected function addDateRange(FormBuilderInterface $builder, array $options, $allowEmpty = true, $required = false): void
{
$params = [
'required' => $required,
'allow_empty' => $allowEmpty,
];
if (\array_key_exists('timezone', $options)) {
$params['timezone'] = $options['timezone'];
}
$builder->add('daterange', DateRangeType::class, $params);
}
protected function addDateRangeChoice(FormBuilderInterface $builder, $allowEmpty = true, $required = false): void
{
$this->addDateRange($builder, [], $allowEmpty, $required);
}
protected function addProjectChoice(FormBuilderInterface $builder, array $options = [], bool $multiCustomer = false, bool $multiActivity = false): void
{
$this->addProjectSelect($builder, $options, false, $multiCustomer, $multiActivity);
}
protected function addProjectMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiCustomer = false, bool $multiActivity = false): void
{
$this->addProjectSelect($builder, $options, true, $multiCustomer, $multiActivity);
}
private function addProjectSelect(FormBuilderInterface $builder, array $options, bool $multiProject, bool $multiCustomer, bool $multiActivity): void
{
$name = 'project';
if ($multiProject) {
$name = 'projects';
}
// just a fake field for having this field at the right position in the frontend
$builder->add($name, ProjectType::class, [
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Project ID'],
'description' => 'Array of project IDs',
],
'choices' => [],
'multiple' => $multiProject,
]);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $options, $name, $multiCustomer, $multiProject, $multiActivity) {
/** @var array<string, mixed> $data */
$data = $event->getData();
$event->getForm()->add($name, ProjectType::class, array_merge([
'multiple' => $multiProject,
'required' => false,
'activity_enabled' => $multiProject ? 'projects[]' : 'project',
'activity_select' => $multiActivity ? 'activities' : 'activity',
'query_builder' => function (ProjectRepository $repo) use ($builder, $data, $options, $multiCustomer, $multiProject) {
$query = new ProjectFormTypeQuery();
$query->setUser($builder->getOption('user'));
$name = $multiCustomer ? 'customers' : 'customer';
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
$customers = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
foreach ($customers as $customer) {
$customer = \is_string($customer) ? (int) $customer : $customer;
if (!\is_int($customer) && !($customer instanceof Customer)) {
throw new \Exception('Need a customer object or an ID for project select');
}
$query->addCustomer($customer);
}
}
$name = $multiProject ? 'projects' : 'project';
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
$projects = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
foreach ($projects as $project) {
$project = \is_string($project) ? (int) $project : $project;
if (!\is_int($project) && !($project instanceof Project)) {
throw new \Exception('Need a project object or an ID for project select');
}
$query->addProject($project);
}
}
if (isset($options['ignore_date']) && true === $options['ignore_date']) {
$query->setIgnoreDate(true);
}
return $repo->getQueryBuilderForFormType($query);
},
], $options));
}
);
}
protected function addActivityChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
{
$this->addActivitySelect($builder, $options, false, $multiProject);
}
protected function addActivityMultiChoice(FormBuilderInterface $builder, array $options = [], bool $multiProject = false): void
{
$this->addActivitySelect($builder, $options, true, $multiProject);
}
private function addActivitySelect(FormBuilderInterface $builder, array $options = [], bool $multiActivity = false, bool $multiProject = false): void
{
$name = 'activity';
if ($multiActivity) {
$name = 'activities';
}
// just a fake field for having this field at the right position in the frontend
$builder->add($name, ActivityType::class, [
'documentation' => [
'type' => 'array',
'items' => ['type' => 'integer', 'description' => 'Activity ID'],
'description' => 'Array of activity IDs',
],
'choices' => [],
'multiple' => $multiActivity,
]);
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($name, $multiActivity, $multiProject) {
/** @var array<string, mixed> $data */
$data = $event->getData();
$event->getForm()->add($name, ActivityType::class, [
'multiple' => $multiActivity,
'required' => false,
'query_builder' => function (ActivityRepository $repo) use ($data, $multiActivity, $multiProject) {
$query = new ActivityFormTypeQuery();
$name = $multiActivity ? 'activities' : 'activity';
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
// we need to pre-fetch the activities to see if they are global, see ActivityFormTypeQuery::isGlobalsOnly()
$activities = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
foreach ($activities as $activity) {
$activity = \is_string($activity) ? (int) $activity : $activity;
if (!\is_int($activity) && !($activity instanceof Activity)) {
throw new \Exception('Need an activity object or an ID for activity select');
}
$query->addActivity($activity);
}
}
$name = $multiProject ? 'projects' : 'project';
if (\array_key_exists($name, $data) && $data[$name] !== null && $data[$name] !== '') {
$projects = \is_array($data[$name]) ? $data[$name] : [$data[$name]];
foreach ($projects as $project) {
$project = \is_string($project) ? (int) $project : $project;
if (!\is_int($project) && !($project instanceof Project)) {
throw new \Exception('Need a project object or an ID for activity select');
}
$query->addProject($project);
}
}
return $repo->getQueryBuilderForFormType($query);
},
]);
}
);
}
protected function addHiddenPagination(FormBuilderInterface $builder): void
{
$builder->add('page', HiddenType::class, [
'documentation' => [
'type' => 'integer',
'description' => 'Page number. Default: 1',
],
'empty_data' => 1
]);
}
protected function addOrder(FormBuilderInterface $builder): void
{
$builder->add('order', ChoiceType::class, [
'documentation' => [
'description' => 'The order for returned items',
],
'label' => 'order',
'choices' => [
'asc' => BaseQuery::ORDER_ASC,
'desc' => BaseQuery::ORDER_DESC
],
'search' => false,
]);
}
protected function addOrderBy(FormBuilderInterface $builder, array $allowedColumns): void
{
$all = [];
foreach ($allowedColumns as $id => $name) {
$label = \is_int($id) ? $name : $id;
$all[$label] = $name;
}
$builder->add('orderBy', ChoiceType::class, [
'label' => 'orderBy',
'choices' => $all,
'search' => false,
]);
}
protected function addTagInputField(FormBuilderInterface $builder): void
{
$builder->add('tags', TagsType::class, [
'required' => false,
'allow_create' => false,
]);
}
protected function addSearchTermInputField(FormBuilderInterface $builder): void
{
$builder->add('searchTerm', SearchTermType::class);
}
protected function addTimesheetStateChoice(FormBuilderInterface $builder): void
{
$builder->add('state', ChoiceType::class, [
'label' => 'entryState',
'required' => false,
'placeholder' => null,
'search' => false,
'choices' => [
'all' => TimesheetQuery::STATE_ALL,
'entryState.running' => TimesheetQuery::STATE_RUNNING,
'entryState.stopped' => TimesheetQuery::STATE_STOPPED
],
]);
}
protected function addExportStateChoice(FormBuilderInterface $builder): void
{
$builder->add('exported', ChoiceType::class, [
'label' => 'exported',
'required' => false,
'placeholder' => null,
'search' => false,
'choices' => [
'all' => TimesheetQuery::STATE_ALL,
'yes' => TimesheetQuery::STATE_EXPORTED,
'no' => TimesheetQuery::STATE_NOT_EXPORTED
],
]);
}
protected function addBillableChoice(FormBuilderInterface $builder): void
{
$builder->add('billable', BillableSearchType::class, [
'required' => false,
'placeholder' => null,
'search' => false,
]);
}
}

View File

@@ -10,33 +10,30 @@
namespace App\Form\Toolbar;
use App\Repository\Query\UserQuery;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Defines the form used for filtering the user.
*/
class UserToolbarForm extends AbstractToolbarForm
final class UserToolbarForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
use ToolbarFormTrait;
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->addSearchTermInputField($builder);
$this->addUserRoleChoice($builder);
$this->addTeamsChoice($builder, 'searchTeams');
$this->addVisibilityChoice($builder, 'label.active');
$this->addVisibilityChoice($builder, 'active');
$this->addPageSizeChoice($builder);
$this->addHiddenPagination($builder);
$this->addOrder($builder);
$this->addOrderBy($builder, UserQuery::USER_ORDER_ALLOWED);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => UserQuery::class,

View File

@@ -24,15 +24,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select an activity.
*/
class ActivityType extends AbstractType
final class ActivityType extends AbstractType
{
private $activityHelper;
private $projectHelper;
public function __construct(ActivityHelper $activityHelper, ProjectHelper $projectHelper)
public function __construct(private ActivityHelper $activityHelper, private ProjectHelper $projectHelper)
{
$this->activityHelper = $activityHelper;
$this->projectHelper = $projectHelper;
}
public function getChoiceLabel(Activity $activity): string
@@ -40,10 +35,7 @@ class ActivityType extends AbstractType
return $this->activityHelper->getChoiceLabel($activity);
}
/**
* {@inheritdoc}
*/
public function groupBy(Activity $activity, $key, $index)
public function groupBy(Activity $activity, $key, $index): ?string
{
if (null === $activity->getProject()) {
return null;
@@ -56,21 +48,18 @@ class ActivityType extends AbstractType
* @param Activity $activity
* @param string $key
* @param mixed $value
* @return array
* @return array<string, string|int|null>
*/
public function getChoiceAttributes(Activity $activity, $key, $value)
public function getChoiceAttributes(Activity $activity, $key, $value): array
{
if (null !== ($project = $activity->getProject())) {
return ['data-project' => $project->getId(), 'data-currency' => $project->getCustomer()->getCurrency()];
return ['data-project' => $project->getId(), 'data-currency' => $project->getCustomer()?->getCurrency()];
}
return [];
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// documentation is for NelmioApiDocBundle
@@ -78,7 +67,7 @@ class ActivityType extends AbstractType
'type' => 'integer',
'description' => 'Activity ID',
],
'label' => 'label.activity',
'label' => 'activity',
'class' => Activity::class,
'choice_label' => [$this, 'getChoiceLabel'],
'choice_attr' => [$this, 'getChoiceAttributes'],
@@ -90,8 +79,17 @@ class ActivityType extends AbstractType
'activities' => null,
// @var Activity|null
'ignore_activity' => null,
'allow_create' => false,
]);
$resolver->setDefault('api_data', function (Options $options) {
if (false !== $options['allow_create']) {
return ['create' => 'post_activity'];
}
return [];
});
$resolver->setDefault('query_builder', function (Options $options) {
return function (ActivityRepository $repo) use ($options) {
$query = new ActivityFormTypeQuery($options['activities'], $options['projects']);
@@ -109,17 +107,14 @@ class ActivityType extends AbstractType
});
}
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-option-pattern' => $this->activityHelper->getChoicePattern(),
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return EntityType::class;
}

View File

@@ -18,26 +18,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Select the pattern that will be used when rendering an activity select.
*/
class ActivityTypePatternType extends AbstractType
final class ActivityTypePatternType extends AbstractType
{
private $translator;
public function __construct(TranslatorInterface $translator)
public function __construct(private TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$name = $this->translator->trans('label.name');
$comment = $this->translator->trans('label.description');
$name = $this->translator->trans('name');
$comment = $this->translator->trans('description');
$spacer = ActivityHelper::SPACER;
$resolver->setDefaults([
'label' => 'label.choice_pattern',
'label' => 'choice_pattern',
'choices' => [
$name => ActivityHelper::PATTERN_NAME,
$comment => ActivityHelper::PATTERN_COMMENT,
@@ -46,10 +40,7 @@ class ActivityTypePatternType extends AbstractType
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return ChoiceType::class;
}

View File

@@ -14,9 +14,9 @@ use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class ArrayToCommaStringType extends AbstractType
final class ArrayToCommaStringType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addModelTransformer(new CallbackTransformer(
function ($value) {
@@ -32,10 +32,7 @@ class ArrayToCommaStringType extends AbstractType
));
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return TextType::class;
}

View File

@@ -13,22 +13,16 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AvatarType extends AbstractType
final class AvatarType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => 'label.avatar',
'label' => 'avatar',
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return TextType::class;
}

View File

@@ -7,33 +7,27 @@
* file that was distributed with this source code.
*/
namespace App\Form\Toolbar;
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class BillableType extends AbstractType
final class BillableSearchType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => 'label.billable',
'label' => 'billable',
'choices' => [
'entryState.all' => null,
'all' => null,
'yes' => true,
'no' => false,
],
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return ChoiceType::class;
}

View File

@@ -13,24 +13,19 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select if something is billable.
* Form field type to select if something is billable.
*/
class BillableType extends AbstractType
final class BillableType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => 'label.billable',
'label' => 'billable',
'help' => 'help.billable',
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return YesNoType::class;
}

View File

@@ -16,31 +16,25 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select the type of budget.
*/
class BudgetType extends AbstractType
final class BudgetType extends AbstractType
{
public const TYPE_MONTH = 'month';
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => 'label.budgetType',
'label' => 'budgetType',
// not yet translated in enough languages
//'placeholder' => 'label.budgetType_full',
//'placeholder' => 'budgetType_full',
'required' => false,
'search' => false,
'choices' => [
'label.budgetType_month' => self::TYPE_MONTH,
'budgetType_month' => self::TYPE_MONTH,
],
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return ChoiceType::class;
}

View File

@@ -17,7 +17,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Select the pattern that will be used for calendar entry titles.
*/
class CalendarTitlePatternType extends AbstractType
final class CalendarTitlePatternType extends AbstractType
{
public const PATTERN_CUSTOMER = '{customer}';
public const PATTERN_PROJECT = '{project}';
@@ -29,22 +29,19 @@ class CalendarTitlePatternType extends AbstractType
public const PATTERN_CUSTOMER_DESCRIPTION = self::PATTERN_CUSTOMER . self::SPACER . self::PATTERN_DESCRIPTION;
public const PATTERN_PROJECT_CUSTOMER = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_CUSTOMER;
private $translator;
public function __construct(TranslatorInterface $translator)
public function __construct(private TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function configureOptions(OptionsResolver $resolver): void
{
$customer = $this->translator->trans('label.customer');
$project = $this->translator->trans('label.project');
$activity = $this->translator->trans('label.activity');
$description = $this->translator->trans('label.description');
$customer = $this->translator->trans('customer');
$project = $this->translator->trans('project');
$activity = $this->translator->trans('activity');
$description = $this->translator->trans('description');
$resolver->setDefaults([
'label' => 'label.choice_pattern',
'label' => 'choice_pattern',
'choices' => [
$activity => CalendarTitlePatternType::PATTERN_ACTIVITY,
$project => CalendarTitlePatternType::PATTERN_PROJECT,

View File

@@ -16,25 +16,17 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a calendar view.
*/
class CalendarViewType extends AbstractType
final class CalendarViewType extends AbstractType
{
public const DEFAULT_VIEW = 'month';
public const ALLOWED_VIEWS = [
'month',
'agendaWeek',
'agendaDay',
];
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$choices = [];
foreach (self::ALLOWED_VIEWS as $name) {
$choices[$name] = $name;
}
$choices = [
'month' => 'month',
'agendaWeek' => 'agendaWeek',
'agendaDay' => 'agendaDay',
];
$resolver->setDefaults([
'required' => true,
@@ -43,10 +35,7 @@ class CalendarViewType extends AbstractType
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return ChoiceType::class;
}

View File

@@ -14,72 +14,53 @@ use App\Constants;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\ColorType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ColorChoiceType extends AbstractType implements DataTransformerInterface
final class ColorChoiceType extends AbstractType implements DataTransformerInterface
{
public const DEFAULT_COLOR = Constants::DEFAULT_COLOR;
private $systemConfiguration;
/**
* @var bool|null
*/
private $limitedColors;
public function __construct(SystemConfiguration $systemConfiguration)
public function __construct(private SystemConfiguration $systemConfiguration)
{
$this->systemConfiguration = $systemConfiguration;
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addViewTransformer($this);
}
private function isLimitedColors(): bool
{
if (null === $this->limitedColors) {
$this->limitedColors = $this->systemConfiguration->isThemeColorsLimited();
}
return $this->limitedColors;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$options = [
'documentation' => [
'type' => 'string',
'description' => sprintf('The hexadecimal color code (default: %s)', self::DEFAULT_COLOR),
'description' => sprintf('The hexadecimal color code (default: %s)', Constants::DEFAULT_COLOR),
],
'label' => 'label.color',
'label' => 'color',
'empty_data' => null,
];
if ($this->isLimitedColors()) {
$choices = [];
$colors = $this->convertStringToColorArray($this->systemConfiguration->getThemeColorChoices());
$choices = [];
$colors = $this->convertStringToColorArray($this->systemConfiguration->getThemeColorChoices());
foreach ($colors as $name => $color) {
$choices[$name] = $color;
}
$options['choices'] = $choices;
$options['search'] = false;
$options['attr']['data-renderer'] = 'color';
foreach ($colors as $name => $color) {
$choices[$name] = $color;
}
$options['choices'] = $choices;
$options['search'] = false;
$resolver->setDefaults($options);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-renderer' => 'color',
]);
}
private function convertStringToColorArray(string $config): array
{
$config = explode(',', $config);
@@ -111,39 +92,18 @@ class ColorChoiceType extends AbstractType implements DataTransformerInterface
return array_unique($colors);
}
/**
* {@inheritdoc}
*/
public function transform($data)
public function transform(mixed $data): mixed
{
if (empty($data) && !$this->isLimitedColors()) {
return self::DEFAULT_COLOR;
}
return $data;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($data)
public function reverseTransform(mixed $value): mixed
{
if (null === $data && !$this->isLimitedColors()) {
return self::DEFAULT_COLOR;
}
return null === $data ? null : $data;
return $value;
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
if ($this->isLimitedColors()) {
return ChoiceType::class;
}
return ColorType::class;
return ChoiceType::class;
}
}

View File

@@ -16,37 +16,28 @@ use Symfony\Component\Form\Extension\Core\Type\ColorType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ColorPickerType extends AbstractType implements DataTransformerInterface
final class ColorPickerType extends AbstractType implements DataTransformerInterface
{
public const DEFAULT_COLOR = Constants::DEFAULT_COLOR;
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addViewTransformer($this);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'documentation' => [
'type' => 'string',
'description' => sprintf('The hexadecimal color code (default: %s)', self::DEFAULT_COLOR),
],
'label' => 'label.color',
'label' => 'color',
'empty_data' => null,
]);
}
/**
* {@inheritdoc}
*/
public function transform($data)
public function transform(mixed $data): mixed
{
if (empty($data)) {
return self::DEFAULT_COLOR;
@@ -55,18 +46,12 @@ class ColorPickerType extends AbstractType implements DataTransformerInterface
return $data;
}
/**
* {@inheritdoc}
*/
public function reverseTransform($data)
public function reverseTransform(mixed $value): mixed
{
return null === $data ? self::DEFAULT_COLOR : $data;
return null === $value ? self::DEFAULT_COLOR : $value;
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return ColorType::class;
}

View File

@@ -24,13 +24,10 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a customer.
*/
class CustomerType extends AbstractType
final class CustomerType extends AbstractType
{
private $customerHelper;
public function __construct(CustomerHelper $customerHelper)
public function __construct(private CustomerHelper $customerHelper)
{
$this->customerHelper = $customerHelper;
}
public function getChoiceLabel(Customer $customer): string
@@ -43,10 +40,7 @@ class CustomerType extends AbstractType
return ['data-currency' => $customer->getCurrency()];
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// documentation is for NelmioApiDocBundle
@@ -54,15 +48,15 @@ class CustomerType extends AbstractType
'type' => 'integer',
'description' => 'Customer ID',
],
'label' => 'label.customer',
'label' => 'customer',
'class' => Customer::class,
'choice_label' => [$this, 'getChoiceLabel'],
'choice_attr' => [$this, 'getChoiceAttributes'],
'query_builder_for_user' => true,
'project_enabled' => false,
'project_select' => 'project',
'start_date_param' => '%begin%',
'end_date_param' => '%end%',
'start_date_param' => '%begin_date%',
'end_date_param' => '%end_date%',
'ignore_date' => false,
'project_visibility' => ProjectQuery::SHOW_VISIBLE,
// @var Customer|null
@@ -108,6 +102,7 @@ class CustomerType extends AbstractType
}
return [
'reload' => 'get_customers',
'select' => $options['project_select'],
'route' => 'get_projects',
'route_params' => $routeParams,
@@ -119,17 +114,14 @@ class CustomerType extends AbstractType
});
}
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-option-pattern' => $this->customerHelper->getChoicePattern(),
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return EntityType::class;
}

View File

@@ -18,28 +18,22 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Select the pattern that will be used when rendering a custom select.
*/
class CustomerTypePatternType extends AbstractType
final class CustomerTypePatternType extends AbstractType
{
private $translator;
public function __construct(TranslatorInterface $translator)
public function __construct(private TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$name = $this->translator->trans('label.name');
$company = $this->translator->trans('label.company');
$number = $this->translator->trans('label.number');
$comment = $this->translator->trans('label.description');
$name = $this->translator->trans('name');
$company = $this->translator->trans('company');
$number = $this->translator->trans('number');
$comment = $this->translator->trans('description');
$spacer = CustomerHelper::SPACER;
$resolver->setDefaults([
'label' => 'label.choice_pattern',
'label' => 'choice_pattern',
'choices' => [
$name => CustomerHelper::PATTERN_NAME,
$company => CustomerHelper::PATTERN_COMPANY,
@@ -56,10 +50,7 @@ class CustomerTypePatternType extends AbstractType
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return ChoiceType::class;
}

View File

@@ -9,11 +9,12 @@
namespace App\Form\Type;
use App\Utils\LocaleSettings;
use App\Configuration\LocaleService;
use App\Utils\FormFormatConverter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
@@ -21,45 +22,51 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
class DatePickerType extends AbstractType
{
private $localeSettings;
public function __construct(LocaleSettings $localeSettings)
public function __construct(private LocaleService $localeService)
{
$this->localeSettings = $localeSettings;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$pickerFormat = $this->localeSettings->getDatePickerFormat();
$dateFormat = $this->localeSettings->getDateTypeFormat();
$builder->addModelTransformer(new CallbackTransformer(
function ($transform) {
return $transform;
},
function ($reverseTransform) use ($options) {
if ($reverseTransform === null) {
return null;
}
if ($reverseTransform instanceof \DateTime && $options['force_time']) {
if ($options['force_time'] === 'start') {
$reverseTransform->setTime(0, 0, 0);
} elseif ($options['force_time'] === 'end') {
$reverseTransform->setTime(23, 59, 59);
}
}
return $reverseTransform;
}
));
}
public function configureOptions(OptionsResolver $resolver): void
{
$format = $this->localeService->getDateFormat(\Locale::getDefault());
$converter = new FormFormatConverter();
$formFormat = $converter->convert($format);
$resolver->setDefaults([
'widget' => 'single_text',
'html5' => false,
'format' => $dateFormat,
'format_picker' => $pickerFormat,
'format' => $formFormat,
'model_timezone' => date_default_timezone_get(),
'view_timezone' => date_default_timezone_get(),
'force_time' => null,
]);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr'] = array_merge($view->vars['attr'], [
'data-datepickerenable' => 'on',
'autocomplete' => 'off',
'placeholder' => strtoupper($options['format']),
'data-format' => $options['format_picker'],
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return DateType::class;
}

View File

@@ -9,11 +9,16 @@
namespace App\Form\Type;
use App\Configuration\LocaleService;
use App\Entity\User;
use App\Form\Model\DateRange;
use App\Utils\LocaleSettings;
use App\Timesheet\DateTimeFactory;
use App\Utils\FormFormatConverter;
use IntlDateFormatter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
@@ -23,105 +28,76 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to select a date range
*/
class DateRangeType extends AbstractType
final class DateRangeType extends AbstractType
{
public const DATE_SPACER = ' - ';
private $localeSettings;
public function __construct(LocaleSettings $localeSettings)
public function __construct(private LocaleService $localeService)
{
$this->localeSettings = $localeSettings;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$pickerFormat = $this->localeSettings->getDatePickerFormat();
$dateFormat = $this->localeSettings->getDateFormat();
$format = $this->localeService->getDateFormat(\Locale::getDefault());
$converter = new FormFormatConverter();
$formFormat = $converter->convert($format);
$pattern = $converter->convertToPattern($formFormat);
$resolver->setDefaults([
'timezone' => date_default_timezone_get(),
'label' => 'label.daterange',
'format' => $dateFormat,
'label' => 'daterange',
'format' => $formFormat,
'separator' => self::DATE_SPACER,
'format_picker' => $pickerFormat,
'allow_empty' => true,
'attr' => [
'pattern' => $pattern . self::DATE_SPACER . $pattern
],
]);
}
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
/** @var User $user */
$user = $options['user'];
$factory = DateTimeFactory::createByUser($user);
$view->vars['ranges'] = [
'yesterday' => [$factory->createDateTime('-1 day 00:00:00'), $factory->createDateTime('-1 day 23:59:59')],
'thisWeek' => [$factory->getStartOfWeek(), $factory->getEndOfWeek()],
'lastWeek' => [$factory->getStartOfWeek('-1 week'), $factory->getEndOfWeek('-1 week')],
'thisMonth' => [$factory->getStartOfMonth(), $factory->getEndOfMonth()],
'lastMonth' => [$factory->getStartOfMonth('-1 month'), $factory->getEndOfMonth('-1 month')],
'thisYear' => [$factory->createStartOfYear(), $factory->createEndOfYear()],
'thisYearUntilNow' => [$factory->createStartOfYear(), $factory->createDateTime('23:59:59')],
'lastYear' => [$factory->createStartOfYear('-1 year'), $factory->createEndOfYear('-1 year')],
'allTime' => [null, null],
];
$view->vars['rangeFormat'] = $options['format'];
$view->vars['attr'] = array_merge($view->vars['attr'], [
'autocomplete' => 'off',
'placeholder' => strtoupper($options['format_picker']) . $options['separator'] . strtoupper($options['format_picker']),
'data-format' => $options['format_picker'],
'data-separator' => $options['separator'],
]);
}
/**
* A better way would be to use the Intl NumberFormatter, but if that is not available
* and the Symfony polyfill is used, this method would not work properly.
*
* @param string $string
* @return string
*/
protected function convertArabicPersian($string)
{
return strtr(
$string,
[
'۰' => '0',
'۱' => '1',
'۲' => '2',
'۳' => '3',
'۴' => '4',
'۵' => '5',
'۶' => '6',
'۷' => '7',
'۸' => '8',
'۹' => '9',
'٠' => '0',
'١' => '1',
'٢' => '2',
'٣' => '3',
'٤' => '4',
'٥' => '5',
'٦' => '6',
'٧' => '7',
'٨' => '8',
'٩' => '9'
]
);
}
protected function formatToPattern(string $format, string $separator)
{
$format = preg_quote($format, '/');
$pattern = str_replace('d', '[0-9]{2}', $format);
$pattern = str_replace('m', '[0-9]{2}', $pattern);
$pattern = str_replace('Y', '[0-9]{4}', $pattern);
return '/^' . $pattern . $separator . $pattern . '$/';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$formatDate = $options['format'];
$separator = $options['separator'];
$allowEmpty = $options['allow_empty'];
$timezone = new \DateTimeZone($options['timezone']);
$pattern = $this->formatToPattern($formatDate, $separator);
$timezone = $options['timezone'];
$pattern = (new FormFormatConverter())->convertToPattern($formatDate . $separator . $formatDate, false);
$builder->addModelTransformer(new CallbackTransformer(
function ($range) use ($formatDate, $separator) {
function ($range) use ($formatDate, $separator, $timezone) {
$dateFormatter = new IntlDateFormatter(
\Locale::getDefault(),
IntlDateFormatter::MEDIUM,
IntlDateFormatter::MEDIUM,
$timezone,
IntlDateFormatter::GREGORIAN,
$formatDate
);
if (null === $range) {
return '';
}
@@ -134,40 +110,46 @@ class DateRangeType extends AbstractType
return '';
}
$display = $range->getBegin()->format($formatDate);
$display = $dateFormatter->format($range->getBegin());
if (null !== $range->getEnd()) {
$display .= $separator . $range->getEnd()->format($formatDate);
$display .= $separator . $dateFormatter->format($range->getEnd());
}
return $display;
},
function ($dates) use ($formatDate, $pattern, $separator, $allowEmpty, $timezone) {
$transformer = new DateTimeToLocalizedStringTransformer(
$timezone,
$timezone,
IntlDateFormatter::MEDIUM,
IntlDateFormatter::MEDIUM,
IntlDateFormatter::GREGORIAN,
$formatDate
);
$range = new DateRange();
if (empty($dates) && $allowEmpty) {
return $range;
}
$dates = $this->convertArabicPersian($dates);
if (preg_match($pattern, $dates) !== 1) {
throw new TransformationFailedException('Invalid date range given');
}
$values = explode($separator, $dates);
if (\count($values) !== 2) {
throw new TransformationFailedException('Invalid date range given');
}
$begin = \DateTime::createFromFormat($formatDate, $values[0], $timezone);
if ($begin === false) {
$begin = $transformer->reverseTransform($values[0]);
if ($begin === null) {
throw new TransformationFailedException('Invalid begin date given');
}
$range->setBegin($begin);
$end = \DateTime::createFromFormat($formatDate, $values[1], $timezone);
if ($end === false) {
$end = $transformer->reverseTransform($values[1]);
if ($end === null) {
throw new TransformationFailedException('Invalid end date given');
}
$range->setEnd($end);
@@ -181,18 +163,12 @@ class DateRangeType extends AbstractType
));
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return TextType::class;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
public function getBlockPrefix(): string
{
return 'daterange';
}

View File

@@ -10,33 +10,73 @@
namespace App\Form\Type;
use App\API\BaseApiController;
use App\Entity\User;
use App\Utils\DateFormatConverter;
use App\Utils\LocaleSettings;
use App\Utils\MomentFormatConverter;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer;
use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Custom form field type to display the date-time input fields.
*/
class DateTimePickerType extends AbstractType
{
private $localeSettings;
public function __construct(LocaleSettings $localeSettings)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$this->localeSettings = $localeSettings;
$parts = ['year', 'month', 'day', 'hour', 'minute'];
$dateParts = ['year', 'month', 'day'];
$timeParts = ['hour', 'minute'];
// Only pass a subset of the options to children
$dateOptions = array_intersect_key($options, array_flip([
'years',
'months',
'days',
'placeholder',
'choice_translation_domain',
'required',
'translation_domain',
'invalid_message',
'invalid_message_parameters',
]));
$timeOptions = array_intersect_key($options, array_flip([
'choice_translation_domain',
'required',
'translation_domain',
'invalid_message',
'invalid_message_parameters',
]));
if (false === $options['label']) {
$dateOptions['label'] = false;
$timeOptions['label'] = false;
}
if (null !== $options['date_label']) {
$dateOptions['label'] = $options['date_label'];
}
if (null !== $options['time_label']) {
$timeOptions['label'] = $options['time_label'];
}
$dateOptions['input'] = $timeOptions['input'] = 'array';
$dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true;
$builder
->addViewTransformer(new DataTransformerChain([
new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts),
new ArrayToPartsTransformer([
'date' => $dateParts,
'time' => $timeParts,
]),
]))
->add('date', DatePickerType::class, $dateOptions)
->add('time', TimePickerType::class, $timeOptions)
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'documentation' => [
@@ -44,49 +84,43 @@ class DateTimePickerType extends AbstractType
'format' => 'date-time',
'example' => (new \DateTime())->format(BaseApiController::DATE_FORMAT_PHP),
],
'label' => 'label.begin',
'widget' => 'single_text',
'html5' => false,
'format' => function (Options $options) {
/** @var User $user */
$user = $options['user'];
$converter = new DateFormatConverter();
return $this->localeSettings->getDateTypeFormat() . ' ' . $converter->convert($user->getTimeFormat()); // PHP
'input' => 'datetime',
'model_timezone' => null,
'view_timezone' => null,
// Don't modify \DateTime classes by reference, we treat
// them like immutable value objects
'by_reference' => false,
'error_bubbling' => false,
// If initialized with a \DateTime object, FormType initializes
// this option to "\DateTime". Since the internal, normalized
// representation is not \DateTime, but an array, we need to unset
// this option.
'data_class' => null,
'compound' => true,
'label' => 'begin',
'date_label' => null,
'time_label' => null,
'empty_data' => function (Options $options) {
return $options['compound'] ? [] : '';
},
'format_picker' => function (Options $options) {
$converter = new MomentFormatConverter();
'invalid_message' => 'Please enter a valid date and time.',
]);
return $converter->convert($options['format']); // JS
},
'with_seconds' => false,
'time_increment' => 1,
// Don't add some defaults in order to preserve the defaults
// set in DateType and TimeType
$resolver->setDefined([
'placeholder',
'choice_translation_domain',
]);
$resolver->setAllowedValues('input', [
'datetime',
'string',
]);
}
public function buildView(FormView $view, FormInterface $form, array $options)
public function getBlockPrefix(): string
{
$attr = array_merge($view->vars['attr'], [
'data-datetimepicker' => 'on',
'autocomplete' => 'off',
'placeholder' => strtoupper($options['format']),
'data-format' => $options['format_picker'],
]);
if ($options['time_increment'] !== null) {
if ($options['time_increment'] >= 1) {
$attr['data-time-picker-increment'] = $options['time_increment'];
}
}
$view->vars['attr'] = $attr;
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return DateTimeType::class;
return 'date_time';
}
}

View File

@@ -12,12 +12,9 @@ namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class DateTimeTextType extends AbstractType
final class DateTimeTextType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return TextType::class;
}

View File

@@ -14,19 +14,16 @@ use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class DayTimeType extends AbstractType
final class DayTimeType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options)
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['attr'] = array_merge($view->vars['attr'], [
'placeholder' => 'hh:mm'
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return TextType::class;
}

View File

@@ -13,22 +13,16 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DescriptionType extends AbstractType
final class DescriptionType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => 'label.description',
'label' => 'description',
]);
}
/**
* {@inheritdoc}
*/
public function getParent()
public function getParent(): string
{
return TextareaType::class;
}

Some files were not shown because too many files have changed in this diff Show More