Release 2.0.5 (#3888)

- Fixed: mandatory fields / form validation for invoice template
- Added: Duration as calendar title replacer (open: running records)
- Fixed: HTML injection in Calendar
- Fixed: Doctrine Proxies are not initialized (leads to empty customer/project)
- Fixed: tag creation
- Fixed: validation errors do not need to be logged in prod
- Removed: Customer VCard download, as used library is outdated and not maintained
- Added: supporting translations domains in many new places (menu, help_text, page_setup, report, exporter, calendar)
This commit is contained in:
Kevin Papst
2023-03-05 03:08:43 +01:00
committed by GitHub
parent 3b93afabc8
commit 0cbf0053d2
77 changed files with 1306 additions and 874 deletions

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\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class HelpTranslationDomainExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['help_translation_domain'] = $options['help_translation_domain'] ?? null;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefined(['help_translation_domain']);
$resolver->setAllowedTypes('help_translation_domain', ['string', 'null']);
$resolver->setDefault('help_translation_domain', null);
}
}

View File

@@ -23,6 +23,7 @@ final class CalendarTitlePatternType extends AbstractType
public const PATTERN_PROJECT = '{project}';
public const PATTERN_ACTIVITY = '{activity}';
public const PATTERN_DESCRIPTION = '{description}';
public const PATTERN_DURATION = '{duration}';
public const SPACER = ' - ';
public const PATTERN_ACTIVITY_DESCRIPTION = self::PATTERN_ACTIVITY . self::SPACER . self::PATTERN_DESCRIPTION;
public const PATTERN_PROJECT_DESCRIPTION = self::PATTERN_PROJECT . self::SPACER . self::PATTERN_DESCRIPTION;
@@ -39,6 +40,7 @@ final class CalendarTitlePatternType extends AbstractType
$project = $this->translator->trans('project');
$activity = $this->translator->trans('activity');
$description = $this->translator->trans('description');
$duration = $this->translator->trans('duration');
$resolver->setDefaults([
'label' => 'choice_pattern',
@@ -47,6 +49,7 @@ final class CalendarTitlePatternType extends AbstractType
$project => CalendarTitlePatternType::PATTERN_PROJECT,
$customer => CalendarTitlePatternType::PATTERN_CUSTOMER,
$description => CalendarTitlePatternType::PATTERN_DESCRIPTION,
$duration => CalendarTitlePatternType::PATTERN_DURATION,
$activity . self::SPACER . $description => CalendarTitlePatternType::PATTERN_ACTIVITY_DESCRIPTION,
$project . self::SPACER . $description => CalendarTitlePatternType::PATTERN_PROJECT_DESCRIPTION,
$customer . self::SPACER . $description => CalendarTitlePatternType::PATTERN_CUSTOMER_DESCRIPTION,

View File

@@ -15,6 +15,9 @@ use App\Repository\TagRepository;
use App\Utils\Color;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\Options;
@@ -25,6 +28,67 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
final class TagsSelectType extends AbstractType
{
public function __construct(private TagRepository $tagRepository)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
if (!$options['allow_create']) {
return;
}
$tagIds = $event->getData();
if (!\is_array($tagIds)) {
return;
}
$ids = array_filter($tagIds, function ($tagId) {
if (is_numeric($tagId)) {
return true;
}
return false;
});
// get the current tags and find the new ones that should be created
$tags = $this->tagRepository->findBy(['id' => $ids]);
$foundIds = [];
foreach ($tags as $tag) {
$foundIds[] = (string) $tag->getId();
}
$newData = [];
$newNames = [];
foreach ($tagIds as $tag) {
if (!\in_array($tag, $foundIds, true)) {
$newNames[] = $tag;
} else {
$newData[] = $tag;
}
}
// in case someone is using tags like "1234" this can interfere with the ID
$tags = $this->tagRepository->findTagsByName($newNames);
$foundTagNames = [];
foreach ($tags as $tag) {
$newData[] = (string) $tag->getId();
$foundTagNames[] = $tag->getName();
}
/** @var array<string> $newNames */
$newNames = array_diff($newNames, $foundTagNames);
foreach ($newNames as $name) {
$tag = new Tag();
$tag->setName($name);
$this->tagRepository->saveTag($tag);
$newData[] = $tag->getId();
}
$event->setData($newData);
}, 1000);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([

View File

@@ -16,8 +16,10 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
final class TagsType extends AbstractType
{
public function __construct(private AuthorizationCheckerInterface $auth, private TagRepository $repository)
{
public function __construct(
private AuthorizationCheckerInterface $auth,
private TagRepository $repository
) {
}
public function configureOptions(OptionsResolver $resolver): void

View File

@@ -52,8 +52,8 @@ final class TimePickerType extends AbstractType
// DateTimePickerType
if ($options['input'] === 'array' && \is_array($data)) {
$now = new \DateTime('now', new \DateTimeZone($options['model_timezone']));
$hour = $data['hour'] === '' ? 0 : (int) $data['hour'];
$minute = $data['minute'] === '' ? 0 : (int) $data['minute'];
$hour = $data['hour'] === '' || !is_numeric($data['hour']) ? 0 : (int) $data['hour'];
$minute = $data['minute'] === '' || !is_numeric($data['minute']) ? 0 : (int) $data['minute'];
$now->setTime($hour, $minute, 0);
$data = $now;
}

View File

@@ -22,6 +22,9 @@ final class TimesheetBillableType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'documentation' => [
'description' => 'Whether this item should be refundable (yes) or not (no) or if it should be calculated by inherited settings from customer, project and activity (auto).',
],
'label' => 'billable',
'choices' => [
'automatic' => Timesheet::BILLABLE_AUTOMATIC,