diff --git a/.changes/unreleased/Feature-20230705-140336.yaml b/.changes/unreleased/Feature-20230705-140336.yaml new file mode 100644 index 000000000..3ce7f3c0f --- /dev/null +++ b/.changes/unreleased/Feature-20230705-140336.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Allow filtering on the basis of a user within general tasks list +time: 2023-07-05T14:03:36.664880092+02:00 +custom: + Issue: "" diff --git a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php index f22b6bfba..51d1b3974 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/Listing/FilterOrderType.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Form\Type\Listing; use Chill\MainBundle\Form\Type\ChillDateType; +use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Templating\Listing\FilterOrderHelper; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; @@ -114,6 +115,28 @@ final class FilterOrderType extends \Symfony\Component\Form\AbstractType $builder->add($singleCheckBoxBuilder); } + + if ([] !== $helper->getUserPickers()) { + $userPickersBuilder = $builder->create('user_pickers', null, ['compound' => true]); + + foreach ($helper->getUserPickers() as $name => [ + 'label' => $label, 'options' => $opts + ]) { + + $userPickersBuilder->add( + $name, + PickUserDynamicType::class, + [ + 'multiple' => true, + 'label' => $label, + ...$opts, + ] + ); + } + + $builder->add($userPickersBuilder); + } + } public static function buildCheckboxChoices(array $choices, array $trans = []): array diff --git a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig index b517eb154..adf67b81b 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/FilterOrder/base.html.twig @@ -18,6 +18,7 @@ {% endif %} + {% if form.dateRanges is defined %} {% set btnSubmit = 1 %} {% if form.dateRanges|length > 0 %} @@ -40,6 +41,7 @@ {% endfor %} {% endif %} {% endif %} + {% if form.checkboxes is defined %} {% set btnSubmit = 1 %} {% if form.checkboxes|length > 0 %} @@ -56,6 +58,7 @@ {% endfor %} {% endif %} {% endif %} + {% if form.entity_choices is defined %} {% set btnSubmit = 1 %} {% if form.entity_choices |length > 0 %} @@ -74,6 +77,25 @@ {% endfor %} {% endif %} {% endif %} + + {% if form.user_pickers is defined %} + {% set btnSubmit = 1 %} + {% if form.user_pickers.children|length > 0 %} + {% for name, options in form.user_pickers %} +
+ {% if form.user_pickers[name].vars.label is not same as(false) %} + {{ form_label(form.user_pickers[name]) }} + {% else %} + {{ form_label(form.user_pickers[name].vars.label) }} + {% endif %} +
+ {{ form_widget(form.user_pickers[name]) }} +
+
+ {% endfor %} + {% endif %} + {% endif %} + {% if form.single_checkboxes is defined %} {% set btnSubmit = 1 %} {% for name, _o in form.single_checkboxes %} @@ -91,8 +113,10 @@ {% endif %} + + {% if active|length > 0 %}
{% for f in active %} diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php index 6b204e552..5b36e52b3 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderGetActiveFilterHelper.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Templating\Listing; +use Chill\MainBundle\Templating\Entity\UserRender; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyAccess\PropertyPathInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -20,6 +21,7 @@ final readonly class FilterOrderGetActiveFilterHelper public function __construct( private TranslatorInterface $translator, private PropertyAccessorInterface $propertyAccessor, + private UserRender $userRender, ) { } @@ -73,6 +75,12 @@ final readonly class FilterOrderGetActiveFilterHelper } } + foreach ($filterOrderHelper->getUserPickers() as $name => ['label' => $label, 'options' => $options]) { + foreach ($filterOrderHelper->getUserPickerData($name) as $user) { + $result[] = ['value' => $this->userRender->renderString($user, []), 'label' => (string) $label, 'position' => FilterOrderPositionEnum::UserPicker->value, 'name' => $name]; + } + } + foreach ($filterOrderHelper->getSingleCheckbox() as $name => ['label' => $label]) { if (true === $filterOrderHelper->getSingleCheckboxData($name)) { $result[] = ['label' => '', 'value' => $this->translator->trans($label), 'position' => FilterOrderPositionEnum::SingleCheckbox->value, 'name' => $name]; diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 84939a052..74d9a7a22 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -11,8 +11,11 @@ declare(strict_types=1); namespace Chill\MainBundle\Templating\Listing; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\Listing\FilterOrderType; use DateTimeImmutable; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -52,6 +55,11 @@ final class FilterOrderHelper private array $entityChoices = []; + /** + * @var array + */ + private array $userPickers = []; + public function __construct( private readonly FormFactoryInterface $formFactory, private readonly RequestStack $requestStack, @@ -80,6 +88,14 @@ final class FilterOrderHelper return $this->entityChoices; } + public function addUserPicker(string $name, ?string $label = null, array $options = []): self + { + $this->userPickers[$name] = ['label' => $label, 'options' => $options]; + + return $this; + } + + public function addCheckbox(string $name, array $choices, ?array $default = [], ?array $trans = [], array $options = []): self { if ([] === $trans) { @@ -114,6 +130,19 @@ final class FilterOrderHelper ->handleRequest($this->requestStack->getCurrentRequest()); } + public function getUserPickers(): array + { + return $this->userPickers; + } + + /** + * @return list + */ + public function getUserPickerData(string $name): array + { + return $this->getFormData()['user_pickers'][$name]; + } + public function hasCheckboxData(string $name): bool { return array_key_exists($name, $this->checkboxes); @@ -203,7 +232,8 @@ final class FilterOrderHelper 'checkboxes' => [], 'dateRanges' => [], 'single_checkboxes' => [], - 'entity_choices' => [] + 'entity_choices' => [], + 'user_pickers' => [] ]; if ($this->hasSearchBox()) { @@ -227,6 +257,10 @@ final class FilterOrderHelper $r['entity_choices'][$name] = ($c['options']['multiple'] ?? true) ? [] : null; } + foreach ($this->userPickers as $name => $u) { + $r['user_pickers'][$name] = ($u['options']['multiple'] ?? true) ? [] : null; + } + return $r; } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index f2bded220..56c73871c 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -39,6 +39,11 @@ class FilterOrderHelperBuilder */ private array $entityChoices = []; + /** + * @var array + */ + private array $userPickers = []; + public function __construct( FormFactoryInterface $formFactory, RequestStack $requestStack, @@ -85,6 +90,13 @@ class FilterOrderHelperBuilder return $this; } + public function addUserPicker(string $name, ?string $label = null, ?array $options = []): self + { + $this->userPickers[$name] = ['label' => $label, 'options' => $options]; + + return $this; + } + public function build(): FilterOrderHelper { $helper = new FilterOrderHelper( @@ -126,6 +138,17 @@ class FilterOrderHelperBuilder $helper->addDateRange($name, $label, $from, $to); } + + foreach ( + $this->userPickers as $name => [ + 'label' => $label, + 'options' => $options + ] + ) { + $helper->addUserPicker($name, $label, $options); + } + + return $helper; } } diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php index 09e8d39aa..ed2337f1d 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderPositionEnum.php @@ -18,4 +18,5 @@ enum FilterOrderPositionEnum: string case DateRange = 'date_range'; case EntityChoice = 'entity_choice'; case SingleCheckbox = 'single_checkbox'; + case UserPicker = 'user_picker'; } diff --git a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php index 2ba9488b6..3523c7a58 100644 --- a/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php +++ b/src/Bundle/ChillTaskBundle/Controller/SingleTaskController.php @@ -26,6 +26,7 @@ use Chill\TaskBundle\Event\TaskEvent; use Chill\TaskBundle\Event\UI\UIEvent; use Chill\TaskBundle\Form\SingleTaskType; use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface; +use Chill\TaskBundle\Repository\SingleTaskStateRepository; use Chill\TaskBundle\Security\Authorization\TaskVoter; use LogicException; use Psr\Log\LoggerInterface; @@ -71,7 +72,8 @@ final class SingleTaskController extends AbstractController EventDispatcherInterface $eventDispatcher, TimelineBuilder $timelineBuilder, LoggerInterface $logger, - FilterOrderHelperFactoryInterface $filterOrderHelperFactory + FilterOrderHelperFactoryInterface $filterOrderHelperFactory, + private SingleTaskStateRepository $singleTaskStateRepository ) { $this->eventDispatcher = $eventDispatcher; $this->timelineBuilder = $timelineBuilder; @@ -299,13 +301,17 @@ final class SingleTaskController extends AbstractController $this->denyAccessUnlessGranted(TaskVoter::SHOW, null); $filterOrder = $this->buildFilterOrder(); + + $filteredUsers = $filterOrder->getUserPickerData('userPicker'); + $flags = array_merge( $filterOrder->getCheckboxData('status'), array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states')) ); $nb = $this->singleTaskAclAwareRepository->countByAllViewable( $filterOrder->getQueryString(), - $flags + $flags, + $filteredUsers ); $paginator = $this->paginatorFactory->create($nb); @@ -313,6 +319,7 @@ final class SingleTaskController extends AbstractController $tasks = $this->singleTaskAclAwareRepository->findByAllViewable( $filterOrder->getQueryString(), $flags, + $filteredUsers, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage(), [ @@ -447,7 +454,7 @@ final class SingleTaskController extends AbstractController { $this->denyAccessUnlessGranted('ROLE_USER'); - $filterOrder = $this->buildFilterOrder(); + $filterOrder = $this->buildFilterOrder(false); $flags = array_merge( $filterOrder->getCheckboxData('status'), array_map(static fn ($i) => 'state_' . $i, $filterOrder->getCheckboxData('states')) @@ -662,7 +669,7 @@ final class SingleTaskController extends AbstractController return $form; } - private function buildFilterOrder(): FilterOrderHelper + private function buildFilterOrder($includeFilterByUser = true): FilterOrderHelper { $statuses = ['no-alert', 'warning', 'alert']; $statusTrans = [ @@ -670,17 +677,22 @@ final class SingleTaskController extends AbstractController 'Tasks near deadline', 'Tasks over deadline', ]; - $states = [ - // todo: get a list of possible states dynamically - 'new', 'in_progress', 'closed', 'canceled', - ]; + $states = $this->singleTaskStateRepository->findAllExistingStates(); + $checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['closed', 'canceled', 'validated'], true))); - return $this->filterOrderHelperFactory + $filterBuilder = $this->filterOrderHelperFactory ->create(self::class) ->addSearchBox() ->addCheckbox('status', $statuses, $statuses, $statusTrans) - ->addCheckbox('states', $states, ['new', 'in_progress']) - ->build(); + ->addCheckbox('states', $states, $checked) + ; + + if ($includeFilterByUser) { + $filterBuilder + ->addUserPicker('userPicker', 'Filter by user', ['multiple' => true, 'required' => false]); + } + + return $filterBuilder->build(); } /** diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php index 0efc16085..d1652cc89 100644 --- a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepository.php @@ -51,7 +51,8 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository public function buildBaseQuery( ?string $pattern = null, - ?array $flags = [] + ?array $flags = [], + ?array $users = [] ): QueryBuilder { $qb = $this->em->createQueryBuilder(); $qb @@ -62,6 +63,24 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository ->setParameter('pattern', '%' . $pattern . '%'); } + if (count($users) > 0) { + $orXUser = $qb->expr()->orX(); + + foreach ($users as $key => $user) { + $orXUser->add( + $qb->expr()->eq('t.assignee', ':user_' . $key) + ); + + $qb->setParameter('user_' . $key, $user); + } + + if ($orXUser->count() > 0) { + $qb->andWhere($orXUser); + } + + return $qb; + } + if (count($flags) > 0) { $orXDate = $qb->expr()->orX(); $orXState = $qb->expr()->orX(); @@ -183,9 +202,10 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository public function countByAllViewable( ?string $pattern = null, - ?array $flags = [] + ?array $flags = [], + ?array $users = [] ): int { - $qb = $this->buildBaseQuery($pattern, $flags); + $qb = $this->buildBaseQuery($pattern, $flags, $users); return $this ->addACLGlobal($qb) @@ -231,11 +251,12 @@ final class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepository public function findByAllViewable( ?string $pattern = null, ?array $flags = [], + ?array $users = [], ?int $start = 0, ?int $limit = 50, ?array $orderBy = [] ): array { - $qb = $this->buildBaseQuery($pattern, $flags); + $qb = $this->buildBaseQuery($pattern, $flags, $users); $qb = $this->addACLGlobal($qb); return $this->getResult($qb, $start, $limit, $orderBy); diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php index 57c57e592..7d2870c67 100644 --- a/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskAclAwareRepositoryInterface.php @@ -18,7 +18,8 @@ interface SingleTaskAclAwareRepositoryInterface { public function countByAllViewable( ?string $pattern = null, - ?array $flags = [] + ?array $flags = [], + ?array $users = [] ): int; public function countByCourse( @@ -38,6 +39,7 @@ interface SingleTaskAclAwareRepositoryInterface public function findByAllViewable( ?string $pattern = null, ?array $flags = [], + ?array $users = [], ?int $start = 0, ?int $limit = 50, ?array $orderBy = [] diff --git a/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php b/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php new file mode 100644 index 000000000..2d64c69a0 --- /dev/null +++ b/src/Bundle/ChillTaskBundle/Repository/SingleTaskStateRepository.php @@ -0,0 +1,47 @@ + + * @throws Exception + */ + public function findAllExistingStates(): array + { + $states = []; + + foreach ($this->connection->fetchAllNumeric(self::FIND_ALL_STATES) as $row) { + if ('' !== $row[0] && null !== $row[0]) { + $states[] = $row[0]; + } + } + + return $states; + } + +} diff --git a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig index 8fe45959e..b479eb948 100644 --- a/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig +++ b/src/Bundle/ChillTaskBundle/Resources/views/SingleTask/List/index.html.twig @@ -27,8 +27,10 @@ {% block css %} {{ parent() }} {{ encore_entry_link_tags('page_task_list') }} + {{ encore_entry_link_tags('mod_pickentity_type') }} {% endblock %} {% block js %} {{ parent() }} {{ encore_entry_script_tags('page_task_list') }} + {{ encore_entry_script_tags('mod_pickentity_type') }} {% endblock %} diff --git a/src/Bundle/ChillTaskBundle/config/services/repositories.yaml b/src/Bundle/ChillTaskBundle/config/services/repositories.yaml index 7bee5abd0..9681e5d5c 100644 --- a/src/Bundle/ChillTaskBundle/config/services/repositories.yaml +++ b/src/Bundle/ChillTaskBundle/config/services/repositories.yaml @@ -1,4 +1,8 @@ services: + _defaults: + autowire: true + autoconfigure: true + chill_task.single_task_repository: class: Chill\TaskBundle\Repository\SingleTaskRepository factory: ['@doctrine.orm.entity_manager', getRepository] @@ -10,8 +14,8 @@ services: - "@chill.main.security.authorization.helper" Chill\TaskBundle\Repository\SingleTaskRepository: '@chill_task.single_task_repository' - Chill\TaskBundle\Repository\SingleTaskAclAwareRepository: - autowire: true - autoconfigure: true + Chill\TaskBundle\Repository\SingleTaskAclAwareRepository: ~ Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface: '@Chill\TaskBundle\Repository\SingleTaskAclAwareRepository' + + Chill\TaskBundle\Repository\SingleTaskStateRepository: ~ diff --git a/src/Bundle/ChillTaskBundle/translations/messages.fr.yml b/src/Bundle/ChillTaskBundle/translations/messages.fr.yml index d437baac2..599ca8640 100644 --- a/src/Bundle/ChillTaskBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillTaskBundle/translations/messages.fr.yml @@ -65,6 +65,7 @@ Not assigned: Aucun utilisateur assigné For person: Pour By: Par Any tasks: Aucune tâche +Filter by user: Filtrer par utilisateur(s) # transitions - default task definition "new": "nouvelle"