Release 2.14 (#4710)

- show "link has expired message" in password reset screen
- added date objects as hydrator variables - for custom date formats in invoice templates
- show meta-fields with null values (e.g. booleans with `false` where hidden)
- fix permission check: allow to remove `view_own_timesheet` but still record times
- prevent error 500 if customer country is empty
- fix API 500 error if project does not exist when creating new timesheet
- fix tags are not created in remote-search mode
- do not check "export items" by default
- fix daterange query, if user an request locale are different
- added logging for invalid SAML responses (see various discussions)
This commit is contained in:
Kevin Papst
2024-04-05 12:38:21 +02:00
committed by GitHub
parent 19b2d47591
commit b6c98f871d
45 changed files with 318 additions and 317 deletions

View File

@@ -14,32 +14,30 @@ use App\Repository\TagRepository;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* @implements DataTransformerInterface<array<Tag>, string>
*/
final class TagArrayToStringTransformer implements DataTransformerInterface
{
private bool $create = true;
public function __construct(private TagRepository $tagRepository)
public function __construct(
private readonly TagRepository $tagRepository,
private readonly bool $create
)
{
}
public function setCreate(bool $create): void
{
$this->create = $create;
}
/**
* Transforms an array of tags to a string.
*
* @param Tag[]|null $tags
* @return string
* @param Tag[]|null $value
*/
public function transform(mixed $tags): mixed
public function transform(mixed $value): string
{
if (empty($tags)) {
if (empty($value)) {
return '';
}
return implode(', ', $tags);
return implode(', ', $value);
}
/**
@@ -47,18 +45,17 @@ final class TagArrayToStringTransformer implements DataTransformerInterface
*
* @see \Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
*
* @param string|null $stringOfTags
* @param string|null $value
* @return Tag[]
* @throws TransformationFailedException
*/
public function reverseTransform(mixed $stringOfTags): mixed
public function reverseTransform(mixed $value): mixed
{
// check for empty tag list
if ('' === $stringOfTags || null === $stringOfTags) {
if ('' === $value || null === $value) {
return [];
}
$names = array_filter(array_unique(array_map('trim', explode(',', $stringOfTags))));
$names = array_filter(array_unique(array_map('trim', explode(',', $value))));
// get the current tags and find the new ones that should be created
$tags = $this->tagRepository->findBy(['name' => $names]);
@@ -69,9 +66,9 @@ final class TagArrayToStringTransformer implements DataTransformerInterface
foreach ($newNames as $name) {
$tag = new Tag();
$tag->setName(mb_substr($name, 0, 100));
$tags[] = $tag;
$this->tagRepository->saveTag($tag);
// new tags persist automatically thanks to the cascade={"persist"}
$tags[] = $tag;
}
}

View File

@@ -73,14 +73,13 @@ trait FormTrait
if ($isNew && \is_int($project)) {
/** @var Project $project */
$project = $repo->find($project);
if ($project === null) {
throw new \Exception('Unknown project');
}
if (!$project->getCustomer()->isVisible()) {
$customer = null;
$project = null;
} elseif (!$project->isVisible()) {
$project = null;
if ($project !== null) {
if (!$project->getCustomer()->isVisible()) {
$customer = null;
$project = null;
} elseif (!$project->isVisible()) {
$project = null;
}
}
}

View File

@@ -42,7 +42,7 @@ final class CustomerToolbarForm extends AbstractType
$countries = $qb->getQuery()->getSingleColumnResult();
$choices = [];
foreach ($countries as $country) {
if (\is_string($country) && \is_string($options['locale'])) {
if (\is_string($country) && $country !== '' && \is_string($options['locale'])) {
$choices[$country] = Countries::getName($country, $options['locale']);
}
}

View File

@@ -65,7 +65,6 @@ final class DateRangeType extends AbstractType
return ['pattern' => $pattern . self::DATE_SPACER . $pattern];
});
}
public function buildView(FormView $view, FormInterface $form, array $options): void

View File

@@ -10,6 +10,7 @@
namespace App\Form\Type;
use App\Form\DataTransformer\TagArrayToStringTransformer;
use App\Repository\TagRepository;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -24,18 +25,17 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
*/
final class TagsInputType extends AbstractType
{
public function __construct(private TagArrayToStringTransformer $transformer, private UrlGeneratorInterface $router)
public function __construct(
private readonly TagRepository $tagRepository,
private readonly UrlGeneratorInterface $router
)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if ($options['allow_create'] === false) {
$this->transformer->setCreate(false);
}
$builder->addModelTransformer(new CollectionToArrayTransformer(), true);
$builder->addModelTransformer($this->transformer, true);
$builder->addModelTransformer(new TagArrayToStringTransformer($this->tagRepository, (bool) $options['allow_create']), true);
}
public function configureOptions(OptionsResolver $resolver): void
@@ -48,6 +48,7 @@ final class TagsInputType extends AbstractType
'allow_create' => false,
'label' => 'tag',
]);
$resolver->setAllowedTypes('allow_create', 'bool');
}
public function buildView(FormView $view, FormInterface $form, array $options): void

View File

@@ -28,78 +28,47 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
*/
final class TagsSelectType extends AbstractType
{
public function __construct(private TagRepository $tagRepository)
public function __construct(
private readonly 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;
}
if (!$options['allow_create']) {
return;
}
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
/** @var array<string> $tagIds */
$tagIds = $event->getData();
if (!\is_array($tagIds)) {
return;
}
$ids = array_filter($tagIds, function ($tagId) {
$tags = [];
foreach ($tagIds as $tagId) {
$tag = null;
if (is_numeric($tagId)) {
return true;
$tag = $this->tagRepository->find($tagId);
}
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 = [];
/** @var array<string> $newNames */
$newNames = [];
foreach ($tagIds as $tag) {
if (!\in_array($tag, $foundIds, true)) {
$newNames[] = $tag;
} else {
$newData[] = $tag;
}
}
// 1. in case someone is using tags like "1234" this can interfere with the ID
// 2. if we would load only visible tags, we would try to create new ones below
// and that would trigger the unique constraint
$tags = $this->tagRepository->findTagsByName($newNames, null);
$foundTagNames = [];
foreach ($tags as $tag) {
$newData[] = (string) $tag->getId();
$foundTagNames[] = $tag->getName();
}
/** @var array<string> $newNamesCreate */
$newNamesCreate = array_udiff($newNames, $foundTagNames, function (mixed $userTag, mixed $existingTag) {
if (!\is_string($userTag) || !\is_string($existingTag)) {
return -1;
if ($tag === null) {
$tag = $this->tagRepository->findTagByName($tagId);
}
if (mb_strtolower($userTag) === mb_strtolower($existingTag)) {
return 0;
if ($tag === null) {
$tag = new Tag();
$tag->setName(mb_substr($tagId, 0, 100));
$this->tagRepository->saveTag($tag);
}
return strcmp($userTag, $existingTag);
});
foreach ($newNamesCreate as $name) {
$tag = new Tag();
$tag->setName(mb_substr($name, 0, 100));
$this->tagRepository->saveTag($tag);
$newData[] = $tag->getId();
$tags[] = $tag->getId();
}
$event->setData($newData);
$event->setData($tags);
}, 1000);
}
@@ -132,6 +101,8 @@ final class TagsSelectType extends AbstractType
return $repo->getQueryBuilderForFormType($query);
};
});
$resolver->setAllowedTypes('allow_create', 'bool');
}
public function buildView(FormView $view, FormInterface $form, array $options): void