Merge branch 'user_filter_tasks' into 'master'

Filter on user within task list

See merge request Chill-Projet/chill-bundles!569
This commit is contained in:
Julien Fastré 2023-07-13 16:07:54 +00:00
commit aea6796ba1
14 changed files with 227 additions and 20 deletions

View File

@ -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: ""

View File

@ -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

View File

@ -18,6 +18,7 @@
</div>
{% endif %}
</div>
{% 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 %}
<div class="row my-2">
{% 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 %}
<div class="col-sm-8 pt-2">
{{ form_widget(form.user_pickers[name]) }}
</div>
</div>
{% endfor %}
{% endif %}
{% endif %}
{% if form.single_checkboxes is defined %}
{% set btnSubmit = 1 %}
{% for name, _o in form.single_checkboxes %}
@ -91,8 +113,10 @@
<button type="submit" class="btn btn-sm btn-misc"><i class="fa fa-fw fa-filter"></i>{{ 'Filter'|trans }}</button>
</div>
{% endif %}
</div>
</div>
{% if active|length > 0 %}
<div class="activeFilters mt-3">
{% for f in active %}

View File

@ -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];

View File

@ -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<string, array{label: string, options: 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<User>
*/
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;
}

View File

@ -39,6 +39,11 @@ class FilterOrderHelperBuilder
*/
private array $entityChoices = [];
/**
* @var array<string, array{label: string, options: 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;
}
}

View File

@ -18,4 +18,5 @@ enum FilterOrderPositionEnum: string
case DateRange = 'date_range';
case EntityChoice = 'entity_choice';
case SingleCheckbox = 'single_checkbox';
case UserPicker = 'user_picker';
}

View File

@ -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();
}
/**

View File

@ -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);

View File

@ -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 = []

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TaskBundle\Repository;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
class SingleTaskStateRepository
{
private const FIND_ALL_STATES = <<<'SQL'
SELECT DISTINCT jsonb_array_elements_text(current_states) FROM chill_task.single_task
SQL;
public function __construct(
private Connection $connection
) {
}
/**
* Return a list of all states associated to at least one single task in the database
*
* @return list<string>
* @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;
}
}

View File

@ -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 %}

View File

@ -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: ~

View File

@ -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"