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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user