Support visibility for tags (#4086)
This commit is contained in:
@@ -180,9 +180,10 @@ final class TimesheetController extends BaseApiController
|
||||
$query->setPageSize((int) $size);
|
||||
}
|
||||
|
||||
/** @var array<string> $tags */
|
||||
$tags = $paramFetcher->get('tags');
|
||||
if (\is_array($tags) && \count($tags) > 0) {
|
||||
$tags = $this->tagRepository->findTagsByName($tags);
|
||||
$tags = $this->tagRepository->findTagsByName($tags, true);
|
||||
foreach ($tags as $tag) {
|
||||
$query->addTag($tag);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ final class TagController extends AbstractController
|
||||
|
||||
$table->addColumn('name', ['class' => 'alwaysVisible']);
|
||||
$table->addColumn('amount', ['class' => 'text-center w-min']);
|
||||
$table->addColumn('visible', ['class' => 'd-none text-center w-min']);
|
||||
$table->addColumn('actions', ['class' => 'actions']);
|
||||
|
||||
$page = new PageSetup('tags');
|
||||
@@ -146,16 +147,59 @@ final class TagController extends AbstractController
|
||||
public function multiDelete(TagRepository $repository, Request $request): Response
|
||||
{
|
||||
$form = $this->getMultiUpdateForm($repository);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
/** @var MultiUpdateTableDTO $dto */
|
||||
$dto = $form->getData();
|
||||
$repository->multiDelete($dto->getEntities());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
if ($form !== null) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
/** @var MultiUpdateTableDTO $dto */
|
||||
$dto = $form->getData();
|
||||
$repository->multiDelete($dto->getEntities());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('tags');
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-invisible', name: 'tags_multi_invisible', methods: ['POST'])]
|
||||
#[IsGranted('manage_tag')]
|
||||
public function multiInvisible(TagRepository $repository, Request $request): Response
|
||||
{
|
||||
return $this->multiUpdateVisible($repository, $request, false);
|
||||
}
|
||||
|
||||
#[Route(path: '/multi-visible', name: 'tags_multi_visible', methods: ['POST'])]
|
||||
#[IsGranted('manage_tag')]
|
||||
public function multiVisible(TagRepository $repository, Request $request): Response
|
||||
{
|
||||
return $this->multiUpdateVisible($repository, $request, true);
|
||||
}
|
||||
|
||||
private function multiUpdateVisible(TagRepository $repository, Request $request, bool $visible): Response
|
||||
{
|
||||
$form = $this->getMultiUpdateForm($repository);
|
||||
|
||||
if ($form !== null) {
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
/** @var MultiUpdateTableDTO $dto */
|
||||
$dto = $form->getData();
|
||||
/** @var Tag $tag */
|
||||
foreach ($dto->getEntities() as $tag) {
|
||||
$tag->setVisible($visible);
|
||||
}
|
||||
$repository->multiUpdate($dto->getEntities());
|
||||
$this->flashSuccess('action.delete.success');
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashDeleteException($ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +209,12 @@ final class TagController extends AbstractController
|
||||
private function getMultiUpdateForm(TagRepository $repository): ?FormInterface
|
||||
{
|
||||
$dto = new MultiUpdateTableDTO();
|
||||
|
||||
if ($this->isGranted('manage_tag')) {
|
||||
$dto->addAction('visible', $this->generateUrl('tags_multi_visible'));
|
||||
$dto->addAction('invisible', $this->generateUrl('tags_multi_invisible'));
|
||||
}
|
||||
|
||||
if ($this->isGranted('delete_tag')) {
|
||||
$dto->addDelete($this->generateUrl('tags_multi_delete'));
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ class Tag
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $name = null;
|
||||
#[ORM\Column(name: 'visible', type: 'boolean', nullable: false, options: ['default' => true])]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private bool $visible = true;
|
||||
|
||||
use ColorTrait;
|
||||
|
||||
@@ -74,6 +79,16 @@ class Tag
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function isVisible(): bool
|
||||
{
|
||||
return $this->visible;
|
||||
}
|
||||
|
||||
public function setVisible(bool $visible): void
|
||||
{
|
||||
$this->visible = $visible;
|
||||
}
|
||||
|
||||
public function addTimesheet(Timesheet $timesheet): void
|
||||
{
|
||||
if ($this->timesheets->contains($timesheet)) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -32,6 +33,10 @@ class TagEditForm extends AbstractType
|
||||
'description' => 'The tag name (forbidden character: comma)',
|
||||
],
|
||||
])
|
||||
->add('visible', YesNoType::class, [
|
||||
'label' => 'visible',
|
||||
'help' => 'help.visible',
|
||||
])
|
||||
;
|
||||
$this->addColor($builder);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ final class TagToolbarForm extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addSearchTermInputField($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
|
||||
@@ -59,6 +59,7 @@ final class TagsSelectType extends AbstractType
|
||||
}
|
||||
|
||||
$newData = [];
|
||||
/** @var array<string> $newNames */
|
||||
$newNames = [];
|
||||
foreach ($tagIds as $tag) {
|
||||
if (!\in_array($tag, $foundIds, true)) {
|
||||
@@ -68,8 +69,10 @@ final class TagsSelectType extends AbstractType
|
||||
}
|
||||
}
|
||||
|
||||
// in case someone is using tags like "1234" this can interfere with the ID
|
||||
$tags = $this->tagRepository->findTagsByName($newNames);
|
||||
// 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();
|
||||
|
||||
@@ -11,12 +11,15 @@ namespace App\Repository\Query;
|
||||
|
||||
class TagQuery extends BaseQuery
|
||||
{
|
||||
use VisibilityTrait;
|
||||
|
||||
public const TAG_ORDER_ALLOWED = ['name', 'amount'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDefaults([
|
||||
'orderBy' => 'name',
|
||||
'visibility' => VisibilityInterface::SHOW_VISIBLE,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ use App\Repository\Paginator\QueryBuilderPaginator;
|
||||
use App\Repository\Query\TagFormTypeQuery;
|
||||
use App\Repository\Query\TagQuery;
|
||||
use App\Utils\Pagination;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
@@ -28,24 +28,14 @@ class TagRepository extends EntityRepository
|
||||
*/
|
||||
public const MAX_AMOUNT_SELECT = 500;
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function saveTag(Tag $tag)
|
||||
public function saveTag(Tag $tag): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->persist($tag);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @throws ORMException
|
||||
* @throws \Doctrine\ORM\OptimisticLockException
|
||||
*/
|
||||
public function deleteTag(Tag $tag)
|
||||
public function deleteTag(Tag $tag): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
$entityManager->remove($tag);
|
||||
@@ -53,26 +43,33 @@ class TagRepository extends EntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $tagNames
|
||||
* @param array<string> $tagNames
|
||||
* @return array<Tag>
|
||||
*/
|
||||
public function findTagsByName(array $tagNames): array
|
||||
public function findTagsByName(array $tagNames, ?bool $visible = null): array
|
||||
{
|
||||
return $this->findBy(['name' => $tagNames]);
|
||||
if ($visible === null) {
|
||||
return $this->findBy(['name' => $tagNames]);
|
||||
}
|
||||
|
||||
return $this->findBy(['name' => $tagNames, 'visible' => $visible]);
|
||||
}
|
||||
|
||||
public function findTagByName(string $tagName): ?Tag
|
||||
public function findTagByName(string $tagName, ?bool $visible = null): ?Tag
|
||||
{
|
||||
return $this->findOneBy(['name' => $tagName]);
|
||||
if ($visible === null) {
|
||||
return $this->findOneBy(['name' => $tagName]);
|
||||
}
|
||||
|
||||
return $this->findOneBy(['name' => $tagName, 'visible' => $visible]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all tag names in an alphabetical order
|
||||
* Find all visible tag names in alphabetical order.
|
||||
*
|
||||
* @param string $filter
|
||||
* @return array
|
||||
* @return array<string>
|
||||
*/
|
||||
public function findAllTagNames($filter = null): array
|
||||
public function findAllTagNames(?string $filter = null): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('t');
|
||||
|
||||
@@ -80,10 +77,12 @@ class TagRepository extends EntityRepository
|
||||
->select('t.name')
|
||||
->addOrderBy('t.name', 'ASC');
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('t.visible', ':visible'));
|
||||
$qb->setParameter('visible', true, ParameterType::BOOLEAN);
|
||||
|
||||
if (null !== $filter) {
|
||||
$qb
|
||||
->andWhere('t.name LIKE :filter')
|
||||
->setParameter('filter', '%' . $filter . '%');
|
||||
$qb->andWhere('t.name LIKE :filter');
|
||||
$qb->setParameter('filter', '%' . $filter . '%');
|
||||
}
|
||||
|
||||
return array_column($qb->getQuery()->getScalarResult(), 'name');
|
||||
@@ -123,7 +122,7 @@ class TagRepository extends EntityRepository
|
||||
{
|
||||
$qb = $this->createQueryBuilder('tag');
|
||||
|
||||
$qb->select('tag.id, tag.name, tag.color, SIZE(tag.timesheets) as amount');
|
||||
$qb->select('tag.id, tag.name, tag.color, tag.visible, SIZE(tag.timesheets) as amount');
|
||||
|
||||
$orderBy = $query->getOrderBy();
|
||||
$orderBy = match ($orderBy) {
|
||||
@@ -131,6 +130,14 @@ class TagRepository extends EntityRepository
|
||||
default => 'tag.' . $orderBy,
|
||||
};
|
||||
|
||||
if ($query->isShowVisible()) {
|
||||
$qb->andWhere($qb->expr()->eq('tag.visible', ':visible'));
|
||||
$qb->setParameter('visible', true, ParameterType::BOOLEAN);
|
||||
} elseif ($query->isShowHidden()) {
|
||||
$qb->andWhere($qb->expr()->eq('tag.visible', ':visible'));
|
||||
$qb->setParameter('visible', false, ParameterType::BOOLEAN);
|
||||
}
|
||||
|
||||
$qb->addOrderBy($orderBy, $query->getOrder());
|
||||
|
||||
if ($query->hasSearchTerm()) {
|
||||
@@ -159,6 +166,8 @@ class TagRepository extends EntityRepository
|
||||
$qb = $this->createQueryBuilder('tag');
|
||||
|
||||
$qb->orderBy('tag.name', 'ASC');
|
||||
$qb->andWhere($qb->expr()->eq('tag.visible', ':visible'));
|
||||
$qb->setParameter('visible', true, ParameterType::BOOLEAN);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
@@ -183,4 +192,25 @@ class TagRepository extends EntityRepository
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag[] $tags
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function multiUpdate(iterable $tags): void
|
||||
{
|
||||
$em = $this->getEntityManager();
|
||||
$em->beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($tags as $tag) {
|
||||
$em->persist($tag);
|
||||
}
|
||||
$em->flush();
|
||||
$em->commit();
|
||||
} catch (\Exception $ex) {
|
||||
$em->rollback();
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user